Compare commits
12 Commits
9587ac85b7
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b7b8802d1 | |||
| b4be492dec | |||
| d25540d727 | |||
| 242e3fde38 | |||
| 5e5727f021 | |||
| 3012507fcc | |||
| 615c324d9b | |||
| 3912eed832 | |||
| e8ab979fd1 | |||
| 69afb0a53e | |||
| b3a2ae4681 | |||
| 6a1b6554f4 |
+18
-19
@@ -1,23 +1,22 @@
|
||||
ignore:
|
||||
- node_modules
|
||||
- dist
|
||||
- "*.log"
|
||||
config:
|
||||
path_mode: relative
|
||||
|
||||
include:
|
||||
- "*.go"
|
||||
- "*.py"
|
||||
- "*.ts"
|
||||
prompts:
|
||||
preset: "下面的代码来自文件 {{path}}:\n---\n{{content}}"
|
||||
|
||||
max_file_size_kb: 200
|
||||
preset:
|
||||
all:
|
||||
- "*"
|
||||
# - "cmd/*"
|
||||
# - "internal/tree/*"
|
||||
# - "internal/utils/*"
|
||||
# - "internal/config/*"
|
||||
# - "internal/resolver/*"
|
||||
# - "main.go"
|
||||
|
||||
bundle:
|
||||
max_tokens: 20000
|
||||
add_file_header: true
|
||||
format: "markdown" # or plain
|
||||
internal:
|
||||
- "internal/tree/*"
|
||||
- "internal/utils/glob.go"
|
||||
|
||||
clipboard:
|
||||
auto_copy: true
|
||||
|
||||
tree:
|
||||
show_size: true
|
||||
max_depth: 5
|
||||
cmd:
|
||||
- "cmd/*"
|
||||
@@ -1 +1,4 @@
|
||||
.git
|
||||
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
@@ -0,0 +1,93 @@
|
||||
# cliprepo
|
||||
|
||||
`cliprepo` is a lightweight, efficient CLI tool written in Go that helps you package your repository's file tree and source code content into a formatted Markdown block. It is designed to quickly build comprehensive context for Large Language Models (LLMs) like GPT, Claude, or DeepSeek.
|
||||
|
||||
## Features
|
||||
|
||||
- **File Tree Generation**: Automatically generates a clean ASCII directory tree, honoring your `.gitignore` rules.
|
||||
- **Flexible File Filtering**: Match files using flexible presets (`*`, `cmd/*`, or specific file paths).
|
||||
- **Gitignore Smart Filtering**: Automatically skips files and directories specified in your `.gitignore` (e.g., `.git`, `node_modules`, `.DS_Store`).
|
||||
- **Clipboard Integration**: Directly copies the generated prompt to your clipboard—no manual file saving required.
|
||||
- **Customizable Templates**: Define how your code blocks are presented to the LLM via a YAML configuration file.
|
||||
|
||||
## Installation
|
||||
|
||||
Ensure you have [Go](https://go.dev/) installed (version 1.26 or higher), then clone the repository and run:
|
||||
|
||||
```bash
|
||||
chmod +x install.sh
|
||||
./install.sh
|
||||
```
|
||||
|
||||
This will compile and install the `cliprepo` binary into your `$GOPATH/bin` directory.
|
||||
|
||||
## Configuration
|
||||
|
||||
Create a `.cliprepo.yaml` file in the root of your project to manage path modes, custom prompt templates, and file group presets:
|
||||
|
||||
```yaml
|
||||
config:
|
||||
path_mode: relative # Options: 'relative' or 'absolute'
|
||||
|
||||
prompts:
|
||||
preset: "The following code is from file {{path}}:\n---\n{{content}}"
|
||||
|
||||
preset:
|
||||
all:
|
||||
- "*" # Scans and includes all tracked files in the repo
|
||||
|
||||
internal:
|
||||
- "internal/tree/*"
|
||||
- "internal/resolver/resolver.go"
|
||||
|
||||
cmd:
|
||||
- "cmd/*"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Run the tool using the `run` command by specifying a preset key defined in your `.cliprepo.yaml`.
|
||||
|
||||
### Print to Terminal
|
||||
|
||||
```bash
|
||||
cliprepo run --delineate cmd
|
||||
```
|
||||
|
||||
### Copy Directly to Clipboard
|
||||
|
||||
```bash
|
||||
cliprepo run --delineate all --clipboard
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
* `--delineate` (string): The preset key to use from `.cliprepo.yaml` (Required).
|
||||
* `--clipboard` (bool): Copy the output directly to your system clipboard instead of printing it to stdout.
|
||||
|
||||
## Output Example
|
||||
|
||||
When running `cliprepo run --delineate cmd`, the output format will look like this:
|
||||
|
||||
```text
|
||||
.
|
||||
├── cmd
|
||||
│ ├── root.go
|
||||
│ └── run.go
|
||||
└── main.go
|
||||
|
||||
The following code is from file cmd/root.go:
|
||||
---
|
||||
package cmd
|
||||
...
|
||||
|
||||
The following code is from file cmd/run.go:
|
||||
---
|
||||
package cmd
|
||||
...
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT License](https://www.google.com/search?q=LICENSE)
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# cliprepo
|
||||
|
||||
`cliprepo` 是一个用 Go 语言编写的轻量、高效的命令行工具(CLI)。它可以将你代码库的目录结构和指定的文件内容一键打包成格式化的 Markdown 文本,专门用于快速为大语言模型(如 GPT、Claude、DeepSeek)构建完整的上下文。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **目录树生成**:自动生成清晰的 ASCII 目录树,并且会自动遵循 `.gitignore` 的过滤规则。
|
||||
- **灵活的文件过滤**:支持通过预设组合(Preset)匹配文件(如 `*`、`cmd/*` 或具体文件路径)。
|
||||
- **智能 Gitignore 过滤**:自动跳过 `.gitignore` 中指定的文件和文件夹(例如 `.git`、`node_modules`、`.DS_Store`)。
|
||||
- **剪贴板集成**:一键将生成的 Prompt 复制到系统剪贴板,省去手动复制或保存文件的烦恼。
|
||||
- **自定义模板**:支持通过 YAML 配置文件自定义代码块向 LLM 展示的模板。
|
||||
|
||||
## 安装指南
|
||||
|
||||
请确保你已经安装了 [Go](https://go.dev/)(版本 1.26 或更高),然后克隆本仓库并执行:
|
||||
|
||||
```bash
|
||||
chmod +x install.sh
|
||||
./install.sh
|
||||
```
|
||||
|
||||
这将会把编译好的 `cliprepo` 二进制文件安装到你的 `$GOPATH/bin` 目录下。
|
||||
|
||||
## 配置文件
|
||||
|
||||
在项目根目录下创建一个 `.cliprepo.yaml` 文件,用于管理路径模式、自定义提示词模板和文件组预设:
|
||||
|
||||
```yaml
|
||||
config:
|
||||
path_mode: relative # 可选值: 'relative' (相对路径) 或 'absolute' (绝对路径)
|
||||
|
||||
prompts:
|
||||
preset: "下面的代码来自文件 {{path}}:\n---\n{{content}}"
|
||||
|
||||
preset:
|
||||
all:
|
||||
- "*" # 扫描并包含库中所有未被 ignore 的文件
|
||||
|
||||
internal:
|
||||
- "internal/tree/*"
|
||||
- "internal/resolver/resolver.go"
|
||||
|
||||
cmd:
|
||||
- "cmd/*"
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
通过 `run` 命令并指定 `.cliprepo.yaml` 中配置的预设(preset)键名来运行该工具。
|
||||
|
||||
### 输出到终端屏幕
|
||||
|
||||
```bash
|
||||
cliprepo run --delineate cmd
|
||||
```
|
||||
|
||||
### 直接复制到剪贴板
|
||||
|
||||
```bash
|
||||
cliprepo run --delineate all --clipboard
|
||||
```
|
||||
|
||||
### 命令行参数(Flags)
|
||||
|
||||
* `--delineate` (string): 指定要使用的 `.cliprepo.yaml` 中的预设键名(必填)。
|
||||
* `--clipboard` (bool): 将生成的结果直接复制到系统剪贴板,不再在终端打印。
|
||||
|
||||
## 输出示例
|
||||
|
||||
当你运行 `cliprepo run --delineate cmd` 时,输出的内容格式如下:
|
||||
|
||||
```text
|
||||
.
|
||||
├── cmd
|
||||
│ ├── root.go
|
||||
│ └── run.go
|
||||
└── main.go
|
||||
|
||||
下面的代码来自文件 cmd/root.go:
|
||||
---
|
||||
package cmd
|
||||
...
|
||||
|
||||
下面的代码来自文件 cmd/run.go:
|
||||
---
|
||||
package cmd
|
||||
...
|
||||
```
|
||||
|
||||
## 开源协议
|
||||
|
||||
[MIT License](https://www.google.com/search?q=LICENSE)
|
||||
@@ -1,36 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"cliprepo/internal/bundle"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var bundleClipboard bool
|
||||
|
||||
var bundleCmd = &cobra.Command{
|
||||
Use: "bundle [key]",
|
||||
Short: "Bundle files by key from config",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
key := args[0]
|
||||
|
||||
result, err := bundle.Build(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bundleClipboard {
|
||||
return bundle.Copy(result)
|
||||
}
|
||||
|
||||
fmt.Println(result)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(bundleCmd)
|
||||
bundleCmd.Flags().BoolVar(&bundleClipboard, "clipboard", false, "Copy to clipboard")
|
||||
}
|
||||
+1
-1
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "cliprepo",
|
||||
Short: "Extract repo structure and bundle code for LLMs",
|
||||
Short: "LLM context builder from repo",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"cliprepo/internal/config"
|
||||
"cliprepo/internal/resolver"
|
||||
"cliprepo/internal/tree"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var delineate string
|
||||
var useClipboard bool
|
||||
|
||||
var runCmd = &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "Generate LLM context from repo",
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
treeStr, err := tree.Generate(".")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString(treeStr)
|
||||
builder.WriteString("\n\n")
|
||||
|
||||
r := resolver.New(".")
|
||||
|
||||
raw, ok := cfg.Preset[delineate]
|
||||
if !ok {
|
||||
return fmt.Errorf("preset not found: %s", delineate)
|
||||
}
|
||||
|
||||
patterns := []string(raw)
|
||||
var files []string
|
||||
|
||||
for _, pattern := range patterns {
|
||||
matches, err := r.ResolvePattern(pattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files = append(files, matches...)
|
||||
}
|
||||
|
||||
files = r.Unique(files)
|
||||
sort.Strings(files)
|
||||
|
||||
for _, file := range files {
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := file
|
||||
if cfg.Config.PathMode == "absolute" {
|
||||
abs, _ := filepath.Abs(file)
|
||||
filePath = abs
|
||||
}
|
||||
|
||||
block := strings.ReplaceAll(template, "{{path}}", filePath)
|
||||
block = strings.ReplaceAll(block, "{{content}}", string(content))
|
||||
|
||||
builder.WriteString(block)
|
||||
builder.WriteString("\n\n")
|
||||
}
|
||||
|
||||
result := builder.String()
|
||||
|
||||
// 6. output
|
||||
if useClipboard {
|
||||
return clipboard.WriteAll(result)
|
||||
}
|
||||
|
||||
fmt.Println(result)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(runCmd)
|
||||
|
||||
runCmd.Flags().StringVar(&delineate, "delineate", "", "preset key")
|
||||
runCmd.Flags().BoolVar(&useClipboard, "clipboard", false, "copy output to clipboard")
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"cliprepo/internal/tree"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var treeOutput string
|
||||
var treeClipboard bool
|
||||
|
||||
var treeCmd = &cobra.Command{
|
||||
Use: "tree",
|
||||
Short: "Print repo tree respecting .gitignore",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
result, err := tree.Generate(".")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if treeClipboard {
|
||||
return tree.Copy(result)
|
||||
}
|
||||
|
||||
if treeOutput != "" {
|
||||
return tree.Write(treeOutput, result)
|
||||
}
|
||||
|
||||
fmt.Println(result)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(treeCmd)
|
||||
|
||||
treeCmd.Flags().StringVarP(&treeOutput, "output", "o", "", "Output file")
|
||||
treeCmd.Flags().BoolVar(&treeClipboard, "clipboard", false, "Copy to clipboard")
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
go install .
|
||||
@@ -1,92 +0,0 @@
|
||||
package bundle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"cliprepo/internal/config"
|
||||
"github.com/atotto/clipboard"
|
||||
)
|
||||
|
||||
const maxFileSize = 1 * 1024 * 1024 // 1MB
|
||||
|
||||
// Build 根据 config 中定义的 bundle key,将多个文件内容聚合成一个字符串
|
||||
//
|
||||
// 设计用途:
|
||||
// 用于 CLI 工具中快速“打包”一组文件内容,例如:
|
||||
// cliprepo bundle api
|
||||
// => 输出 api 相关的一组代码文件内容,方便复制/分享/提交 prompt
|
||||
//
|
||||
// 行为特点:
|
||||
// 1. 从 cliprepo.yaml 中读取 bundles 配置
|
||||
// 2. 根据 key 找到对应文件列表
|
||||
// 3. 逐个读取文件内容并拼接
|
||||
// 4. 自动跳过异常文件(不存在 / 读取失败)
|
||||
// 5. 自动过滤过大的文件(> 1MB)
|
||||
//
|
||||
// 输出格式:
|
||||
//
|
||||
// ===== path/to/file =====
|
||||
// <file content>
|
||||
//
|
||||
// ===== path/to/another =====
|
||||
// <file content>
|
||||
func Build(key string) (string, error) {
|
||||
// 读取配置文件(cliprepo.yaml)
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 获取对应 bundle 的文件列表
|
||||
files, ok := cfg.Bundles[key]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("bundle not found: %s", key)
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
|
||||
// 遍历 bundle 中的所有文件路径
|
||||
for _, file := range files {
|
||||
// 获取文件信息(用于判断大小 / 是否存在)
|
||||
info, err := os.Stat(file)
|
||||
if err != nil {
|
||||
// 文件不存在或无法访问时跳过
|
||||
continue
|
||||
}
|
||||
|
||||
// 跳过过大的文件,避免 CLI 输出过载或内存占用过高
|
||||
if info.Size() > maxFileSize {
|
||||
continue
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
// 读取失败直接跳过,不中断整个 bundle 构建
|
||||
continue
|
||||
}
|
||||
|
||||
// 写入文件分隔标记,便于阅读结构
|
||||
builder.WriteString(fmt.Sprintf("===== %s =====\n", file))
|
||||
|
||||
// 写入文件内容
|
||||
builder.Write(data)
|
||||
builder.WriteString("\n\n")
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
// Copy 将 bundle 内容写入系统剪贴板
|
||||
//
|
||||
// 常见使用场景:
|
||||
// CLI 一键复制 bundle 输出,用于:
|
||||
// - prompt 输入
|
||||
// - 代码审查
|
||||
// - 远程分享
|
||||
// - AI 上下文输入
|
||||
func Copy(content string) error {
|
||||
return clipboard.WriteAll(content)
|
||||
}
|
||||
+46
-20
@@ -1,38 +1,64 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config 定义 cliprepo 的配置结构
|
||||
//
|
||||
// 当前支持的配置:
|
||||
// Bundles:用于定义一组文件路径集合
|
||||
// key -> bundle 名称
|
||||
// value -> 对应的文件路径列表
|
||||
type Config struct {
|
||||
Bundles map[string][]string `yaml:"bundles"`
|
||||
type StringOrSlice []string
|
||||
|
||||
func (s *StringOrSlice) UnmarshalYAML(value *yaml.Node) error {
|
||||
switch value.Kind {
|
||||
|
||||
// "preset: *"
|
||||
case yaml.ScalarNode:
|
||||
*s = []string{value.Value}
|
||||
return nil
|
||||
|
||||
// "preset: [a, b]"
|
||||
case yaml.SequenceNode:
|
||||
var out []string
|
||||
for _, v := range value.Content {
|
||||
out = append(out, v.Value)
|
||||
}
|
||||
*s = out
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("invalid preset format")
|
||||
}
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Config struct {
|
||||
PathMode string `yaml:"path_mode"`
|
||||
} `yaml:"config"`
|
||||
|
||||
Prompts struct {
|
||||
Preset string `yaml:"preset"`
|
||||
} `yaml:"prompts"`
|
||||
|
||||
Preset map[string]StringOrSlice `yaml:"preset"`
|
||||
}
|
||||
|
||||
// Load 从项目根目录读取 cliprepo.yaml 配置文件,并解析为 Config 结构
|
||||
//
|
||||
// 行为说明:
|
||||
// 1. 默认读取当前工作目录下的 cliprepo.yaml
|
||||
// 2. 如果文件不存在或读取失败,直接返回 error
|
||||
// 3. 使用 YAML 解析配置内容到 Config 结构体
|
||||
func Load() (*Config, error) {
|
||||
// 读取配置文件内容
|
||||
data, err := os.ReadFile("cliprepo.yaml")
|
||||
data, err := os.ReadFile(".cliprepo.yaml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
|
||||
// 解析 YAML 到结构体
|
||||
err = yaml.Unmarshal(data, &cfg)
|
||||
|
||||
return &cfg, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// DEFAULT PROMPT TEMPLATE
|
||||
if cfg.Prompts.Preset == "" {
|
||||
cfg.Prompts.Preset = "The following code is from file {{path}}:\n---\n{{content}}"
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package gitignore
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
ignore "github.com/sabhiram/go-gitignore"
|
||||
)
|
||||
|
||||
// Load 加载指定 root 目录下的 .gitignore 规则
|
||||
//
|
||||
// 行为说明:
|
||||
// 1. 如果 root/.gitignore 存在,则解析该文件
|
||||
// 2. 如果不存在,则返回一个“空规则集”(不会忽略任何文件)
|
||||
// 3. 返回 go-gitignore 的 GitIgnore 对象,用于路径匹配
|
||||
func Load(root string) (*ignore.GitIgnore, error) {
|
||||
// 拼接 .gitignore 文件路径
|
||||
path := filepath.Join(root, ".gitignore")
|
||||
|
||||
// 如果 .gitignore 不存在,则返回一个空的 ignore 规则
|
||||
// 这样后续 MatchesPath 不会 panic,也不会过滤任何文件
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return ignore.CompileIgnoreLines(), nil
|
||||
}
|
||||
|
||||
// 如果存在 .gitignore,则解析文件内容生成规则
|
||||
return ignore.CompileIgnoreFile(path)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"cliprepo/internal/tree"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Resolver struct {
|
||||
root string
|
||||
seen map[string]struct{}
|
||||
}
|
||||
|
||||
func New(root string) *Resolver {
|
||||
return &Resolver{
|
||||
root: root,
|
||||
seen: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// ResolvePattern 智能解析各种 pattern 并过滤 ignore 文件
|
||||
func (r *Resolver) ResolvePattern(pattern string) ([]string, error) {
|
||||
if pattern == "*" {
|
||||
return r.WalkAll()
|
||||
}
|
||||
|
||||
var matches []string
|
||||
// 处理类似 cmd/* 或 internal/tree/* 的情况
|
||||
// 如果包含 * 且不是以 ** 开头,转换为通过 filepath.Walk 匹配或直接 Glob 过滤
|
||||
rawMatches, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ig, _ := tree.Load(r.root)
|
||||
|
||||
for _, m := range rawMatches {
|
||||
info, err := os.Stat(m)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
rel, _ := filepath.Rel(r.root, m)
|
||||
// 统一检查 .gitignore
|
||||
if ig != nil && ig.MatchesPath(rel) {
|
||||
continue
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
// 如果匹配到的是目录(比如 internal/* 匹配到了 internal/config),则深层遍历该目录
|
||||
err := filepath.Walk(m, func(path string, fInfo os.FileInfo, walkErr error) error {
|
||||
if walkErr != nil || fInfo.IsDir() {
|
||||
return nil
|
||||
}
|
||||
fRel, _ := filepath.Rel(r.root, path)
|
||||
if ig != nil && ig.MatchesPath(fRel) {
|
||||
return nil
|
||||
}
|
||||
r.add(fRel, &matches)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
r.add(rel, &matches)
|
||||
}
|
||||
}
|
||||
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
// WalkAll 遍历全库并过滤
|
||||
func (r *Resolver) WalkAll() ([]string, error) {
|
||||
var result []string
|
||||
ig, _ := tree.Load(r.root)
|
||||
|
||||
err := filepath.Walk(r.root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if info.IsDir() && info.Name() == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
rel, _ := filepath.Rel(r.root, path)
|
||||
|
||||
if ig != nil && ig.MatchesPath(rel) {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
r.add(rel, &result)
|
||||
return nil
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (r *Resolver) AddFile(path string, result *[]string) {
|
||||
r.add(path, result)
|
||||
}
|
||||
|
||||
func (r *Resolver) add(path string, result *[]string) {
|
||||
if _, ok := r.seen[path]; ok {
|
||||
return
|
||||
}
|
||||
r.seen[path] = struct{}{}
|
||||
*result = append(*result, path)
|
||||
}
|
||||
|
||||
func (r *Resolver) Unique(in []string) []string {
|
||||
seen := make(map[string]struct{})
|
||||
out := make([]string, 0, len(in))
|
||||
|
||||
for _, f := range in {
|
||||
if _, ok := seen[f]; ok {
|
||||
continue
|
||||
}
|
||||
seen[f] = struct{}{}
|
||||
out = append(out, f)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package tree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
ignore "github.com/sabhiram/go-gitignore"
|
||||
)
|
||||
|
||||
// Load 加载 .gitignore
|
||||
func Load(root string) (*ignore.GitIgnore, error) {
|
||||
path := filepath.Join(root, ".gitignore")
|
||||
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return ignore.CompileIgnoreLines(), nil
|
||||
}
|
||||
|
||||
return ignore.CompileIgnoreFile(path)
|
||||
}
|
||||
+4
-37
@@ -5,73 +5,40 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"cliprepo/internal/gitignore"
|
||||
"github.com/atotto/clipboard"
|
||||
)
|
||||
|
||||
// Generate 从 root 目录开始遍历文件树,并生成类似 `tree` 命令的目录结构字符串
|
||||
//
|
||||
// 功能特点:
|
||||
// 1. 递归遍历目录
|
||||
// 2. 支持 .gitignore 规则过滤
|
||||
// 3. 输出树形结构字符串
|
||||
func Generate(root string) (string, error) {
|
||||
// 加载 gitignore 规则(如果失败则忽略错误,默认不过滤)
|
||||
ig, _ := gitignore.Load(root)
|
||||
|
||||
ig, _ := Load(root)
|
||||
|
||||
var builder strings.Builder
|
||||
|
||||
// 递归遍历目录结构
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 根目录特殊处理,统一显示为 "."
|
||||
if path == root {
|
||||
builder.WriteString(".\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 转换为相对路径,便于匹配 gitignore 和计算层级
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
|
||||
// 如果路径匹配 gitignore 规则,则跳过
|
||||
// 如果是目录,则直接跳过整个子树
|
||||
if ig.MatchesPath(rel) {
|
||||
// gitignore filter
|
||||
if ig != nil && ig.MatchesPath(rel) {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 计算当前节点在目录树中的层级
|
||||
// 通过路径分隔符数量判断深度
|
||||
level := strings.Count(rel, string(os.PathSeparator))
|
||||
|
||||
// 根据层级生成缩进
|
||||
prefix := strings.Repeat("│ ", level)
|
||||
|
||||
// 写入当前节点(文件或目录)
|
||||
builder.WriteString(fmt.Sprintf("%s├── %s\n", prefix, info.Name()))
|
||||
return nil
|
||||
})
|
||||
|
||||
return builder.String(), err
|
||||
}
|
||||
|
||||
// Copy 将生成的字符串写入系统剪贴板
|
||||
//
|
||||
// 常用于 CLI 工具:直接复制 tree 输出结果
|
||||
func Copy(content string) error {
|
||||
return clipboard.WriteAll(content)
|
||||
}
|
||||
|
||||
// Write 将内容写入指定文件路径
|
||||
//
|
||||
// 如果文件存在则覆盖
|
||||
// 权限默认设置为 0644(rw-r--r--)
|
||||
func Write(path, content string) error {
|
||||
return os.WriteFile(path, []byte(content), 0644)
|
||||
}
|
||||
Reference in New Issue
Block a user