mirror of
https://github.com/tnypxl/rollup.git
synced 2025-12-13 06:23:18 +00:00
feat: add files subcommand and refactor rollup functionality
This commit is contained in:
186
cmd/files.go
Normal file
186
cmd/files.go
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
path string
|
||||||
|
fileTypes string
|
||||||
|
codeGenPatterns string
|
||||||
|
ignorePatterns string
|
||||||
|
)
|
||||||
|
|
||||||
|
var filesCmd = &cobra.Command{
|
||||||
|
Use: "files",
|
||||||
|
Short: "Rollup files into a single Markdown file",
|
||||||
|
Long: `The files subcommand writes the contents of all files (with target custom file types provided)
|
||||||
|
in a given project, current path or a custom path, to a single timestamped markdown file
|
||||||
|
whose name is <project-directory-name>-rollup-<timestamp>.md.`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runRollup()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
filesCmd.Flags().StringVarP(&path, "path", "p", ".", "Path to the project directory")
|
||||||
|
filesCmd.Flags().StringVarP(&fileTypes, "types", "t", ".go,.md,.txt", "Comma-separated list of file extensions to include")
|
||||||
|
filesCmd.Flags().StringVarP(&codeGenPatterns, "codegen", "g", "", "Comma-separated list of glob patterns for code-generated files")
|
||||||
|
filesCmd.Flags().StringVarP(&ignorePatterns, "ignore", "i", "", "Comma-separated list of glob patterns for files to ignore")
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchGlob(pattern, path string) bool {
|
||||||
|
parts := strings.Split(pattern, "/")
|
||||||
|
return matchGlobRecursive(parts, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchGlobRecursive(patternParts []string, path string) bool {
|
||||||
|
if len(patternParts) == 0 {
|
||||||
|
return path == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if patternParts[0] == "**" {
|
||||||
|
for i := 0; i <= len(path); i++ {
|
||||||
|
if matchGlobRecursive(patternParts[1:], path[i:]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
i := strings.IndexByte(path, '/')
|
||||||
|
if i < 0 {
|
||||||
|
matched, _ := filepath.Match(patternParts[0], path)
|
||||||
|
return matched && len(patternParts) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
matched, _ := filepath.Match(patternParts[0], path[:i])
|
||||||
|
return matched && matchGlobRecursive(patternParts[1:], path[i+1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCodeGenerated(filePath string, patterns []string) bool {
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
if strings.Contains(pattern, "**") {
|
||||||
|
if matchGlob(pattern, filePath) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
matched, err := filepath.Match(pattern, filepath.Base(filePath))
|
||||||
|
if err == nil && matched {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIgnored(filePath string, patterns []string) bool {
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
if strings.Contains(pattern, "**") {
|
||||||
|
if matchGlob(pattern, filePath) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
matched, err := filepath.Match(pattern, filepath.Base(filePath))
|
||||||
|
if err == nil && matched {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRollup() error {
|
||||||
|
// Use config if available, otherwise use command-line flags
|
||||||
|
var types, codeGenList, ignoreList []string
|
||||||
|
if cfg != nil && len(cfg.FileTypes) > 0 {
|
||||||
|
types = cfg.FileTypes
|
||||||
|
} else {
|
||||||
|
types = strings.Split(fileTypes, ",")
|
||||||
|
}
|
||||||
|
if cfg != nil && len(cfg.CodeGenerated) > 0 {
|
||||||
|
codeGenList = cfg.CodeGenerated
|
||||||
|
} else {
|
||||||
|
codeGenList = strings.Split(codeGenPatterns, ",")
|
||||||
|
}
|
||||||
|
if cfg != nil && cfg.Ignore != nil && len(cfg.Ignore) > 0 {
|
||||||
|
ignoreList = cfg.Ignore
|
||||||
|
} else {
|
||||||
|
ignoreList = strings.Split(ignorePatterns, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the absolute path
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting absolute path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the project directory name
|
||||||
|
projectName := filepath.Base(absPath)
|
||||||
|
|
||||||
|
// Generate the output file name
|
||||||
|
timestamp := time.Now().Format("20060102-150405")
|
||||||
|
outputFileName := fmt.Sprintf("%s-%s.rollup.md", projectName, timestamp)
|
||||||
|
|
||||||
|
// Open the output file
|
||||||
|
outputFile, err := os.Create(outputFileName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating output file: %v", err)
|
||||||
|
}
|
||||||
|
defer outputFile.Close()
|
||||||
|
|
||||||
|
// Walk through the directory
|
||||||
|
err = filepath.Walk(absPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
if strings.HasPrefix(info.Name(), ".") {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
relPath, _ := filepath.Rel(absPath, path)
|
||||||
|
|
||||||
|
// Check if the file should be ignored
|
||||||
|
if isIgnored(relPath, ignoreList) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := filepath.Ext(path)
|
||||||
|
for _, t := range types {
|
||||||
|
if ext == "."+t {
|
||||||
|
// Read file contents
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error reading file %s: %v", path, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the file is code-generated
|
||||||
|
isCodeGen := isCodeGenerated(relPath, codeGenList)
|
||||||
|
codeGenNote := ""
|
||||||
|
if isCodeGen {
|
||||||
|
codeGenNote = " (Code-generated, Read-only)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write file name and contents to the output file
|
||||||
|
fmt.Fprintf(outputFile, "# File: %s%s\n\n```%s\n%s```\n\n", relPath, codeGenNote, t, string(content))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error walking through directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Rollup complete. Output file: %s", outputFileName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
203
cmd/root.go
203
cmd/root.go
@@ -2,53 +2,22 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
config "github.com/tnypxl/rollup/internal/config"
|
config "github.com/tnypxl/rollup/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
path string
|
configFile string
|
||||||
fileTypes string
|
cfg *config.Config
|
||||||
codeGenPatterns string
|
verbose bool
|
||||||
ignorePatterns string
|
|
||||||
configFile string
|
|
||||||
cfg *config.Config
|
|
||||||
verbose bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "rollup",
|
Use: "rollup",
|
||||||
Short: "Rollup files into a single Markdown file",
|
Short: "Rollup is a tool for combining and processing files",
|
||||||
Long: `Rollup is a tool that writes the contents of all files (with target custom file types provided)
|
Long: `Rollup is a versatile tool that can combine and process files in various ways.
|
||||||
in a given project, current path or a custom path, to a single timestamped markdown file
|
Use subcommands to perform specific operations.`,
|
||||||
whose name is <project-directory-name>-rollup-<timestamp>.md.`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
// Skip config loading and rollup execution for help command
|
|
||||||
if cmd.Name() == "help" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if configFile == "" {
|
|
||||||
defaultConfig := config.DefaultConfigPath()
|
|
||||||
if config.FileExists(defaultConfig) {
|
|
||||||
configFile = defaultConfig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if configFile != "" {
|
|
||||||
var err error
|
|
||||||
cfg, err = config.Load(configFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error loading config file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return runRollup()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute(conf *config.Config) error {
|
func Execute(conf *config.Config) error {
|
||||||
@@ -60,160 +29,8 @@ func Execute(conf *config.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.Flags().StringVarP(&path, "path", "p", ".", "Path to the project directory")
|
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "f", "", "Path to the config file (default: rollup.yml in the current directory)")
|
||||||
rootCmd.Flags().StringVarP(&fileTypes, "types", "t", ".go,.md,.txt", "Comma-separated list of file extensions to include")
|
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging")
|
||||||
rootCmd.Flags().StringVarP(&codeGenPatterns, "codegen", "g", "", "Comma-separated list of glob patterns for code-generated files")
|
|
||||||
rootCmd.Flags().StringVarP(&ignorePatterns, "ignore", "i", "", "Comma-separated list of glob patterns for files to ignore")
|
rootCmd.AddCommand(filesCmd)
|
||||||
rootCmd.Flags().StringVarP(&configFile, "config", "f", "", "Path to the config file (default: rollup.yml in the current directory)")
|
|
||||||
rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging")
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchGlob(pattern, path string) bool {
|
|
||||||
parts := strings.Split(pattern, "/")
|
|
||||||
return matchGlobRecursive(parts, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchGlobRecursive(patternParts []string, path string) bool {
|
|
||||||
if len(patternParts) == 0 {
|
|
||||||
return path == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if patternParts[0] == "**" {
|
|
||||||
for i := 0; i <= len(path); i++ {
|
|
||||||
if matchGlobRecursive(patternParts[1:], path[i:]) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
i := strings.IndexByte(path, '/')
|
|
||||||
if i < 0 {
|
|
||||||
matched, _ := filepath.Match(patternParts[0], path)
|
|
||||||
return matched && len(patternParts) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
matched, _ := filepath.Match(patternParts[0], path[:i])
|
|
||||||
return matched && matchGlobRecursive(patternParts[1:], path[i+1:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func isCodeGenerated(filePath string, patterns []string) bool {
|
|
||||||
for _, pattern := range patterns {
|
|
||||||
if strings.Contains(pattern, "**") {
|
|
||||||
if matchGlob(pattern, filePath) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
matched, err := filepath.Match(pattern, filepath.Base(filePath))
|
|
||||||
if err == nil && matched {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isIgnored(filePath string, patterns []string) bool {
|
|
||||||
for _, pattern := range patterns {
|
|
||||||
if strings.Contains(pattern, "**") {
|
|
||||||
if matchGlob(pattern, filePath) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
matched, err := filepath.Match(pattern, filepath.Base(filePath))
|
|
||||||
if err == nil && matched {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func runRollup() error {
|
|
||||||
// Use config if available, otherwise use command-line flags
|
|
||||||
var types, codeGenList, ignoreList []string
|
|
||||||
if cfg != nil && len(cfg.FileTypes) > 0 {
|
|
||||||
types = cfg.FileTypes
|
|
||||||
} else {
|
|
||||||
types = strings.Split(fileTypes, ",")
|
|
||||||
}
|
|
||||||
if cfg != nil && len(cfg.CodeGenerated) > 0 {
|
|
||||||
codeGenList = cfg.CodeGenerated
|
|
||||||
} else {
|
|
||||||
codeGenList = strings.Split(codeGenPatterns, ",")
|
|
||||||
}
|
|
||||||
if cfg != nil && cfg.Ignore != nil && len(cfg.Ignore) > 0 {
|
|
||||||
ignoreList = cfg.Ignore
|
|
||||||
} else {
|
|
||||||
ignoreList = strings.Split(ignorePatterns, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the absolute path
|
|
||||||
absPath, err := filepath.Abs(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error getting absolute path: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the project directory name
|
|
||||||
projectName := filepath.Base(absPath)
|
|
||||||
|
|
||||||
// Generate the output file name
|
|
||||||
timestamp := time.Now().Format("20060102-150405")
|
|
||||||
outputFileName := fmt.Sprintf("%s-%s.rollup.md", projectName, timestamp)
|
|
||||||
|
|
||||||
// Open the output file
|
|
||||||
outputFile, err := os.Create(outputFileName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error creating output file: %v", err)
|
|
||||||
}
|
|
||||||
defer outputFile.Close()
|
|
||||||
|
|
||||||
// Walk through the directory
|
|
||||||
err = filepath.Walk(absPath, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if info.IsDir() {
|
|
||||||
if strings.HasPrefix(info.Name(), ".") {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
relPath, _ := filepath.Rel(absPath, path)
|
|
||||||
|
|
||||||
// Check if the file should be ignored
|
|
||||||
if isIgnored(relPath, ignoreList) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ext := filepath.Ext(path)
|
|
||||||
for _, t := range types {
|
|
||||||
if ext == "."+t {
|
|
||||||
// Read file contents
|
|
||||||
content, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error reading file %s: %v", path, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the file is code-generated
|
|
||||||
isCodeGen := isCodeGenerated(relPath, codeGenList)
|
|
||||||
codeGenNote := ""
|
|
||||||
if isCodeGen {
|
|
||||||
codeGenNote = " (Code-generated, Read-only)"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write file name and contents to the output file
|
|
||||||
fmt.Fprintf(outputFile, "# File: %s%s\n\n```%s\n%s```\n\n", relPath, codeGenNote, t, string(content))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error walking through directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Rollup complete. Output file: %s", outputFileName)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user