├── .gitignore ├── .github └── workflows │ └── release.yml ├── main.go ├── README.md ├── internal └── worktree │ ├── worktree.go │ └── commands │ └── pr │ └── pr.go ├── go.mod ├── LICENSE └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | /gh-workspace 2 | /gh-workspace.exe 3 | gh-worktree 4 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: cli/gh-extension-precompile@v1 15 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/despreston/gh-worktree/internal/worktree" 8 | ) 9 | 10 | func main() { 11 | cmd, err := worktree.New() 12 | if err != nil { 13 | fmt.Fprintln(os.Stderr, err) 14 | os.Exit(1) 15 | } 16 | 17 | if err := cmd.Execute(); err != nil { 18 | fmt.Fprintln(os.Stderr, err) 19 | os.Exit(1) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | It's an extension for github.com/cli/cli that adds some stuff for managing git worktrees. 2 | 3 | ```sh 4 | commands to create and manage git worktrees 5 | 6 | Usage: 7 | worktree [flags] 8 | worktree [command] 9 | 10 | Available Commands: 11 | completion Generate the autocompletion script for the specified shell 12 | help Help about any command 13 | pr worktree from PR 14 | 15 | Flags: 16 | -h, --help help for worktree 17 | 18 | Use "worktree [command] --help" for more information about a command. 19 | ``` 20 | -------------------------------------------------------------------------------- /internal/worktree/worktree.go: -------------------------------------------------------------------------------- 1 | package worktree 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cli/go-gh" 7 | "github.com/despreston/gh-worktree/internal/worktree/commands/pr" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func New() (*cobra.Command, error) { 12 | rest, err := gh.RESTClient(nil) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | var rootCmd = &cobra.Command{ 18 | Use: "worktree", 19 | Short: "Git worktrees, dawg", 20 | Long: "commands to create and manage git worktrees", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | fmt.Println("root worktree command") 23 | }, 24 | } 25 | 26 | rootCmd.AddCommand(pr.New(rest)) 27 | return rootCmd, nil 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/despreston/gh-worktree 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/cli/go-gh v1.2.1 7 | github.com/cli/safeexec v1.0.0 8 | github.com/spf13/cobra v1.4.0 9 | ) 10 | 11 | require ( 12 | github.com/cli/shurcooL-graphql v0.0.2 // indirect 13 | github.com/henvic/httpretty v0.0.6 // indirect 14 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 15 | github.com/kr/text v0.2.0 // indirect 16 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 17 | github.com/mattn/go-isatty v0.0.16 // indirect 18 | github.com/mattn/go-runewidth v0.0.13 // indirect 19 | github.com/muesli/termenv v0.12.0 // indirect 20 | github.com/rivo/uniseg v0.2.0 // indirect 21 | github.com/spf13/pflag v1.0.5 // indirect 22 | github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect 23 | golang.org/x/net v0.7.0 // indirect 24 | golang.org/x/sys v0.5.0 // indirect 25 | golang.org/x/term v0.5.0 // indirect 26 | gopkg.in/yaml.v3 v3.0.1 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Des Preston 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/worktree/commands/pr/pr.go: -------------------------------------------------------------------------------- 1 | package pr 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | 9 | "github.com/cli/go-gh" 10 | ghapi "github.com/cli/go-gh/pkg/api" 11 | "github.com/cli/safeexec" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func New(restClient ghapi.RESTClient) *cobra.Command { 16 | cmd := &cobra.Command{ 17 | Use: "pr [pr number] ", 18 | Short: "worktree from PR", 19 | Long: "Create a new worktree from a PR number", 20 | Args: func(cmd *cobra.Command, args []string) error { 21 | if len(args) < 1 { 22 | return errors.New("requires a pr number") 23 | } 24 | return nil 25 | }, 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | repo, err := gh.CurrentRepository() 28 | if err != nil { 29 | return err 30 | } 31 | 32 | branchName, err := getPullRequest(restClient, repo.Owner(), repo.Name(), args[0]) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | path := branchName 38 | if len(args) > 1 { 39 | path = args[1] 40 | } 41 | 42 | prNumber := args[0] 43 | return createWorktreeFromPR(prNumber, branchName, path) 44 | }, 45 | } 46 | 47 | return cmd 48 | } 49 | 50 | func getPullRequest(rc ghapi.RESTClient, owner, repo, pr string) (string, error) { 51 | var response = struct { 52 | Head struct { 53 | Ref string 54 | } 55 | }{} 56 | 57 | url := fmt.Sprintf("repos/%s/%s/pulls/%s", owner, repo, pr) 58 | if err := rc.Get(url, &response); err != nil { 59 | return "", err 60 | } 61 | 62 | return response.Head.Ref, nil 63 | } 64 | 65 | func createWorktreeFromPR(prNumber, branchName string, path string) error { 66 | // First, fetch the PR ref from GitHub 67 | fetchCmd := []string{"git", "fetch", "origin", fmt.Sprintf("pull/%s/head:%s", prNumber, branchName)} 68 | 69 | exe, err := safeexec.LookPath(fetchCmd[0]) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | fetch := exec.Command(exe, fetchCmd[1:]...) 75 | fetch.Stdout = os.Stdout 76 | fetch.Stderr = os.Stderr 77 | 78 | if err := fetch.Run(); err != nil { 79 | return fmt.Errorf("failed to fetch PR: %w", err) 80 | } 81 | 82 | // Now create the worktree from the fetched branch 83 | worktreeCmd := []string{"git", "worktree", "add", path, branchName} 84 | 85 | worktree := exec.Command(exe, worktreeCmd[1:]...) 86 | worktree.Stdout = os.Stdout 87 | worktree.Stderr = os.Stderr 88 | 89 | return worktree.Run() 90 | } 91 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= 2 | github.com/cli/go-gh v1.2.1 h1:xFrjejSsgPiwXFP6VYynKWwxLQcNJy3Twbu82ZDlR/o= 3 | github.com/cli/go-gh v1.2.1/go.mod h1:Jxk8X+TCO4Ui/GarwY9tByWm/8zp4jJktzVZNlTW5VM= 4 | github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= 5 | github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= 6 | github.com/cli/shurcooL-graphql v0.0.2 h1:rwP5/qQQ2fM0TzkUTwtt6E2LbIYf6R+39cUXTa04NYk= 7 | github.com/cli/shurcooL-graphql v0.0.2/go.mod h1:tlrLmw/n5Q/+4qSvosT+9/W5zc8ZMjnJeYBxSdb4nWA= 8 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 9 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= 12 | github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs= 13 | github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= 14 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 15 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 16 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 17 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 18 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 19 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 20 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 21 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 22 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= 23 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 24 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 25 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 26 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 27 | github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc= 28 | github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 31 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 32 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 33 | github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= 34 | github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= 35 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 36 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 37 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 38 | github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= 39 | github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= 40 | golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 41 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= 42 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 43 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 47 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 48 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 49 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 50 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 51 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 52 | golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= 53 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 54 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 55 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 56 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 57 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 58 | gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= 59 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 60 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 61 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 62 | --------------------------------------------------------------------------------