├── .github └── workflows │ ├── go.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── main.go └── main_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.14 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | 29 | - name: Build 30 | run: go build -v . 31 | 32 | - name: Test 33 | run: go test -v . 34 | 35 | lint: 36 | name: "Run static analysis" 37 | runs-on: "ubuntu-latest" 38 | steps: 39 | - uses: actions/setup-go@v2 40 | with: 41 | go-version: ^1.14 42 | - run: "GO111MODULE=on go get honnef.co/go/tools/cmd/staticcheck" 43 | - uses: actions/checkout@v1 44 | with: 45 | fetch-depth: 1 46 | - run: "go vet ./..." 47 | - run: "$(go env GOPATH)/bin/staticcheck -go 1.11 ./..." -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | - 18 | name: Set up Go 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: ^1.15 22 | - 23 | name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v2 25 | with: 26 | version: latest 27 | args: release --rm-dist 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | builds: 5 | - id: "git-get" 6 | binary: "git-get" 7 | env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - linux 11 | - windows 12 | - darwin 13 | archives: 14 | - replacements: 15 | darwin: Darwin 16 | linux: Linux 17 | windows: Windows 18 | 386: i386 19 | amd64: x86_64 20 | format_overrides: 21 | - goos: windows 22 | format: zip 23 | 24 | checksum: 25 | name_template: 'checksums.txt' 26 | snapshot: 27 | name_template: "{{ .Tag }}-next" 28 | changelog: 29 | sort: asc 30 | filters: 31 | exclude: 32 | - '^Github:' 33 | - '^dev:' 34 | - '^README:' 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Joonas Kuorilehto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-get 2 | 3 | `git-get` is a helper that allows cloning relative URLs with a short hand 4 | 5 | $ git get joneskoo/git-get 6 | 7 | Regardless of working directory where `git get` is executed, this expands to: 8 | 9 | $ git clone git@github.com:joneskoo/git-get ~/src/github.com/joneskoo/git-get 10 | 11 | This allows easy cloning of repositories into an uniform 12 | directory structure. 13 | 14 | [![Go](https://github.com/joneskoo/git-get/workflows/Go/badge.svg)](https://github.com/joneskoo/git-get/actions?query=workflow%3AGo) 15 | 16 | ## Installing 17 | 18 | $ go get -u github.com/joneskoo/git-get 19 | 20 | Make sure `git-get` is in your `PATH`; by default go get 21 | installs to `$HOME/bin`. `git` will automatically understand 22 | `git get` after this, but `git-get` is also valid. 23 | 24 | ## Configuration 25 | 26 | You can override the defaults by setting environment variables: 27 | 28 | | Environment variable | Default | Description | 29 | | -------------------- | ----------------- | ------------------------------------------ | 30 | | `GIT_GET_PREFIX` | `git@github.com:` | Prefix to add to relative clone targets | 31 | | `GIT_GET_ROOT` | `~/src` | Clone destination directory root | 32 | 33 | ## Usage 34 | 35 | $ git get joneskoo/git-get 36 | $ git get git@github.com:joneskoo/git-get 37 | $ git get https://github.com/joneskoo/git-get 38 | 39 | These all clone to same directory. 40 | 41 | Pro tip: combine **git-get** with *CDPATH* in your shell. If you set in your `.zshrc` or `.bashrc`: 42 | 43 | ```bash 44 | CDPATH=$HOME/src:$HOME/src/github.com:$HOME/src/github.com/joneskoo:. 45 | ``` 46 | 47 | You can use any of these commands to `cd` into `/home/joneskoo/src/github.com/joneskoo/git-get` 48 | from anywhere! 49 | 50 | $ cd git-get 51 | $ cd joneskoo/git-get 52 | $ cd github.com/joneskoo/git-get 53 | 54 | But not only that, you can use cd to your other favorite projects as everything 55 | is cloned to the same directory structure. As you can also clone with absolute 56 | URLs, this works fine if you use this for work repositories but occasionally 57 | clone some open source project. 58 | 59 | **WARNING: this is highly addictive and you will not be able to work without this after trying it.** 60 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/joneskoo/git-get 2 | 3 | go 1.14 4 | 5 | require github.com/mitchellh/go-homedir v1.1.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 2 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 3 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Command git-get clones Git repositories with an implicitly relative URL 2 | // and always to a path under source root regardless of working directory. 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/mitchellh/go-homedir" 14 | ) 15 | 16 | const ( 17 | // sourceRoot is the target prefix where we clone to, 18 | // can be overridden with environment variable GIT_GET_ROOT. 19 | defaultTargetPath = "~/src" 20 | 21 | // defaultPrefix is prefixed to implicitly relative clone URLs, 22 | // can be overriden with environment variable GIT_GET_PREFIX. 23 | defaultPrefix = "git@github.com:" 24 | ) 25 | 26 | const usage = ` 27 | 28 | Usage: git-get (URL|PROJECT/REPOSITORY) 29 | 30 | $ git get joneskoo/git-get # PROJECT/REPOSITORY 31 | $ git get git@github.com:joneskoo/git-get # URL 32 | 33 | Regardless of working directory where git get is executed, this expands to: 34 | 35 | $ git clone git@github.com:joneskoo/git-get ~/src/github.com/joneskoo/git-get 36 | 37 | This allows easy cloning of repositories into an uniform directory structure. 38 | ` 39 | 40 | func main() { 41 | logger := log.New(os.Stderr, "git-get: ", 0) 42 | 43 | if len(os.Args) != 2 { 44 | logger.Fatalln(usage) 45 | } 46 | relativeCloneURL := os.Args[1] 47 | 48 | targetPath := defaultTargetPath 49 | if s := os.Getenv("GIT_GET_ROOT"); s != "" { 50 | targetPath = s 51 | } 52 | var err error 53 | targetPath, err = homedir.Expand(targetPath) 54 | if err != nil { 55 | logger.Fatalf("failed to expand target path: %v", err) 56 | } 57 | 58 | prefix := defaultPrefix 59 | if s := os.Getenv("GIT_GET_PREFIX"); s != "" { 60 | prefix = s 61 | } 62 | cloneURL := expand(relativeCloneURL, prefix) 63 | td, err := targetDir(cloneURL) 64 | if err != nil { 65 | logger.Fatal(err) 66 | } 67 | 68 | // Replace current process with git 69 | cmd := exec.Command("git", "clone", cloneURL, filepath.Join(targetPath, td)) 70 | cmd.Stdin = os.Stdin 71 | cmd.Stderr = os.Stderr 72 | cmd.Stdout = os.Stdout 73 | err = cmd.Run() 74 | if ee, ok := err.(*exec.ExitError); ok { 75 | os.Exit(ee.ExitCode()) 76 | } 77 | if err != nil { 78 | logger.Fatalf("calling git failed: %v", err) 79 | } 80 | } 81 | 82 | // expand completes the implicitly relative clone URL s of form "project/repo" into 83 | // absolute clone URL. 84 | // 85 | // If s does not have the required form, it is returned unchanged. 86 | // unmodified. 87 | func expand(s, defaultPrefix string) string { 88 | if strings.Contains(s, ":") { 89 | return s 90 | } 91 | parts := strings.SplitN(s, "/", 2) 92 | if len(parts) < 2 { 93 | return s 94 | } 95 | return defaultPrefix + parts[0] + "/" + parts[1] + ".git" 96 | } 97 | 98 | // targetDir resolves the cloneURL to a relative directory path. 99 | func targetDir(cloneURL string) (string, error) { 100 | cleanedCloneURL := strings.TrimSuffix(cloneURL, ".git") 101 | 102 | var hostname, path string 103 | 104 | // URLs like https:// and ssh:// 105 | if parts := strings.SplitN(cleanedCloneURL, "://", 2); len(parts) == 2 { 106 | if addressParts := strings.SplitN(parts[1], "/", 2); len(addressParts) == 2 { 107 | hostname = addressParts[0] 108 | path = addressParts[1] 109 | } else { 110 | return "", fmt.Errorf(`expected path in URL, got %q`, cloneURL) 111 | } 112 | // URLs like user@hostname:project/repo 113 | } else if parts := strings.SplitN(cleanedCloneURL, ":", 2); len(parts) == 2 { 114 | hostname = parts[0] 115 | path = parts[1] 116 | } else { 117 | return "", fmt.Errorf(`expected PROJECT/REPO or absolute git clone URL, got %q`, cloneURL) 118 | } 119 | 120 | // ignore username 121 | parts := strings.Split(hostname, "@") 122 | hostname = parts[len(parts)-1] 123 | 124 | pathparts := strings.Split(path, "/") 125 | target := append([]string{hostname}, pathparts...) 126 | return strings.ToLower(filepath.Join(target...)), nil 127 | } 128 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func Test_expand(t *testing.T) { 6 | type args struct { 7 | input string 8 | defaultPrefix string 9 | } 10 | tests := []struct { 11 | name string 12 | args args 13 | want string 14 | }{ 15 | { 16 | "plain name without project or protocol", 17 | args{"hello", "git@github.com:"}, 18 | "hello", 19 | }, 20 | { 21 | "absolute HTTPS URI should be unmodified", 22 | args{"https://github.com/joneskoo/git-get", "git@github.com:"}, 23 | "https://github.com/joneskoo/git-get", 24 | }, 25 | { 26 | "absolute short ssh address should be unmodified", 27 | args{"git@github.com:joneskoo/git-get.git", "git@github.com:"}, 28 | "git@github.com:joneskoo/git-get.git", 29 | }, 30 | 31 | { 32 | "absolute ssh URI should be unmodified", 33 | args{"ssh://git@github.com:joneskoo/git-get.git", "git@github.com:"}, 34 | "ssh://git@github.com:joneskoo/git-get.git", 35 | }, 36 | { 37 | "relative github path should complete", 38 | args{"joneskoo/git-get", "git@github.com:"}, 39 | "git@github.com:joneskoo/git-get.git", 40 | }, 41 | { 42 | "relative https path should complete", 43 | args{"joneskoo/git-get", "https://example.com/"}, 44 | "https://example.com/joneskoo/git-get.git", 45 | }, 46 | } 47 | for _, tt := range tests { 48 | t.Run(tt.name, func(t *testing.T) { 49 | if got := expand(tt.args.input, tt.args.defaultPrefix); got != tt.want { 50 | t.Errorf("expand(%q, %q) = %q, want %q", tt.args.input, tt.args.defaultPrefix, got, tt.want) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func Test_targetDir(t *testing.T) { 57 | type args struct { 58 | cloneURL string 59 | } 60 | tests := []struct { 61 | name string 62 | args args 63 | want string 64 | wantErr bool 65 | }{ 66 | { 67 | name: "non-URL should return error", 68 | args: args{"foobar"}, 69 | wantErr: true, 70 | }, 71 | { 72 | name: "plain hostname is not valid", 73 | args: args{"https://github.com"}, 74 | wantErr: true, 75 | }, 76 | { 77 | name: "ssh short form address", 78 | args: args{"user@hostname:project/repo"}, 79 | want: "hostname/project/repo", 80 | }, 81 | { 82 | name: "ssh short form address with git suffix", 83 | args: args{"user@hostname:project/repo.git"}, 84 | want: "hostname/project/repo", 85 | }, 86 | { 87 | name: "ok https url", 88 | args: args{"https://github.com/joneskoo/git-get"}, 89 | want: "github.com/joneskoo/git-get", 90 | }, 91 | { 92 | name: "ok https url with git suffix", 93 | args: args{"https://github.com/joneskoo/git-get.git"}, 94 | want: "github.com/joneskoo/git-get", 95 | }, 96 | { 97 | name: "ok ssh url", 98 | args: args{"ssh://git@github.com/joneskoo/git-get"}, 99 | want: "github.com/joneskoo/git-get", 100 | }, 101 | { 102 | name: "ok ssh url with git suffix", 103 | args: args{"ssh://git@github.com/joneskoo/git-get.git"}, 104 | want: "github.com/joneskoo/git-get", 105 | }, 106 | } 107 | for _, tt := range tests { 108 | t.Run(tt.name, func(t *testing.T) { 109 | got, err := targetDir(tt.args.cloneURL) 110 | if (err != nil) != tt.wantErr { 111 | t.Errorf("targetDir() error = %v, wantErr %v", err, tt.wantErr) 112 | return 113 | } 114 | if got != tt.want { 115 | t.Errorf("targetDir(%q) = %v, want %v", tt.args.cloneURL, got, tt.want) 116 | } 117 | }) 118 | } 119 | } 120 | --------------------------------------------------------------------------------