diff --git a/README.md b/README.md index b285f46..0e2f095 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ Example output with `dirmd -o docs.md ~/repos/myproject --vertical-slices`: Output is designed for RAG ingestion and LLM context windows, not human presentation: - **ISO 8601 Timestamp**: Added as a metadata line (`> Generated: ...`) immediately after the H1 header for every new file created. +- **Git Commit Info**: If the source directory is a Git repository, a second metadata line (`> Commit: ()`) is added below the timestamp. - **ASCII tree format** (`+-` instead of Unicode box-drawing) to reduce token overhead - **Explicit file delimiters**: `--- FILE: ./path (bytes, lines) ---` and `--- END FILE ---` - **Document type markers**: `[ROOT]` and `[SLICE]` in H1 headers @@ -230,6 +231,7 @@ Example output structure (non-sliced mode): # /home/user/repos/myproject [ROOT] > Generated: 2026-09-02T14:30:00Z + > Commit: a1b2c3d (Fix login bug) ## Directory Tree diff --git a/cmd/root.go b/cmd/root.go index c002cb9..4c71c39 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -30,7 +30,7 @@ Examples: # Create new documentation (writes index.md inside ./docs/) dirmd -o ./docs ~/repos/myproject - # Overwrite existing output directory + # Overwrite existing output directory (DELETES everything in it first) dirmd -o ./docs ~/repos/myproject -f # Append a single file @@ -86,7 +86,7 @@ func Execute() { func init() { rootCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", "", "output directory (required)") rootCmd.Flags().BoolVarP(&appendMode, "append", "a", false, "append to existing index.md") - rootCmd.Flags().BoolVarP(&force, "force", "f", false, "overwrite existing output directory") + rootCmd.Flags().BoolVarP(&force, "force", "f", false, "overwrite existing output directory (deletes all contents)") rootCmd.Flags().StringVar(&singleFile, "input-file", "", "single file input (append mode only)") rootCmd.Flags().BoolVarP(&tuiMode, "interactive", "i", false, "launch interactive TUI configuration wizard") rootCmd.Flags().Int64Var(&maxSize, "max-size", config.DefaultMaxSize, "max file size in bytes") @@ -173,9 +173,16 @@ func run(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to resolve output path: %w", err) } + // Handle output directory existence if !appendMode { - if _, err := os.Stat(absOutput); err == nil && !force { - return fmt.Errorf("output directory %s already exists (use -f to overwrite)", absOutput) + if _, err := os.Stat(absOutput); err == nil { + if !force { + return fmt.Errorf("output directory %s already exists (use -f to overwrite)", absOutput) + } + // FORCE MODE: Always delete the entire directory and recreate it fresh + if err := os.RemoveAll(absOutput); err != nil { + return fmt.Errorf("failed to remove existing output directory: %w", err) + } } } diff --git a/internal/git/git.go b/internal/git/git.go new file mode 100644 index 0000000..fc19e40 --- /dev/null +++ b/internal/git/git.go @@ -0,0 +1,37 @@ +package git + +import ( + "fmt" + "os/exec" + "strings" +) + +type CommitInfo struct { + ShortSHA string + Message string +} + +func GetCommitInfo(rootDir string) *CommitInfo { + cmd := exec.Command("git", "-C", rootDir, "log", "-1", "--format=%h %s") + output, err := cmd.Output() + if err != nil { + return nil + } + + parts := strings.SplitN(strings.TrimSpace(string(output)), " ", 2) + if len(parts) < 2 { + return nil + } + + return &CommitInfo{ + ShortSHA: parts[0], + Message: parts[1], + } +} + +func FormatCommitLine(info *CommitInfo) string { + if info == nil { + return "" + } + return fmt.Sprintf("> Commit: %s (%s)\n", info.ShortSHA, info.Message) +} diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go index c5c2cdb..a9aa397 100644 --- a/internal/renderer/renderer.go +++ b/internal/renderer/renderer.go @@ -9,6 +9,7 @@ import ( "time" "unicode/utf8" + "git.flo.fo/FLO/dirmd/internal/git" "git.flo.fo/FLO/dirmd/internal/tree" "git.flo.fo/FLO/dirmd/internal/walker" ) @@ -38,9 +39,10 @@ func Write(result *walker.Result, outputPath string, appendMode bool, singleFile fmt.Fprintf(&sb, "# %s [ROOT]\n", h1Path) - // Add timestamp only if creating a new file (not append mode) if !appendMode { - fmt.Fprintf(&sb, "> Generated: %s\n\n", time.Now().Format(time.RFC3339)) + fmt.Fprintf(&sb, "> Generated: %s\n", time.Now().Format(time.RFC3339)) + sb.WriteString(git.FormatCommitLine(result.Commit)) + sb.WriteString("\n") } if !singleFile { @@ -116,7 +118,7 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e rootPath := filepath.Join(outputDir, indexBasename) generated = append(generated, rootPath) - if err := writeRootSlice(rootPath, absRoot, rootName, repoName, rootFiles, topLevelDirs, result.OmitTest); err != nil { + if err := writeRootSlice(rootPath, absRoot, rootName, repoName, rootFiles, topLevelDirs, result.OmitTest, result.Commit); err != nil { return nil, fmt.Errorf("failed to write root slice: %w", err) } @@ -151,7 +153,7 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e } } - if err := writeDirSlice(slicePath, absDir, absRoot, dirName, treeEntries, dirEntries, indexBasename, result.OmitTest); err != nil { + if err := writeDirSlice(slicePath, absDir, absRoot, dirName, treeEntries, dirEntries, indexBasename, result.OmitTest, result.Commit); err != nil { return nil, fmt.Errorf("failed to write slice %s: %w", sliceName, err) } } @@ -159,7 +161,7 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e return generated, nil } -func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker.Entry, topLevelDirs map[string][]walker.Entry, omitTest bool) error { +func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker.Entry, topLevelDirs map[string][]walker.Entry, omitTest bool, commit *git.CommitInfo) error { f, err := os.Create(path) if err != nil { return fmt.Errorf("cannot create root file: %w", err) @@ -169,8 +171,9 @@ func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker var sb strings.Builder fmt.Fprintf(&sb, "# %s [ROOT]\n", absRoot) - // Timestamp for new file creation - fmt.Fprintf(&sb, "> Generated: %s\n\n", time.Now().Format(time.RFC3339)) + fmt.Fprintf(&sb, "> Generated: %s\n", time.Now().Format(time.RFC3339)) + sb.WriteString(git.FormatCommitLine(commit)) + sb.WriteString("\n") treeLines := buildTreeWithRefs(rootName, repoName, topLevelDirs, rootFiles) sb.WriteString("## Directory Tree\n\n") @@ -238,7 +241,7 @@ func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker return err } -func writeDirSlice(path, absDir, absRoot, dirName string, treeEntries, fileEntries []walker.Entry, rootFileName string, omitTest bool) error { +func writeDirSlice(path, absDir, absRoot, dirName string, treeEntries, fileEntries []walker.Entry, rootFileName string, omitTest bool, commit *git.CommitInfo) error { f, err := os.Create(path) if err != nil { return fmt.Errorf("cannot create slice file: %w", err) @@ -248,8 +251,8 @@ func writeDirSlice(path, absDir, absRoot, dirName string, treeEntries, fileEntri var sb strings.Builder fmt.Fprintf(&sb, "# %s [SLICE]\n", absDir) - // Timestamp for new file creation - fmt.Fprintf(&sb, "> Generated: %s\n\n", time.Now().Format(time.RFC3339)) + fmt.Fprintf(&sb, "> Generated: %s\n", time.Now().Format(time.RFC3339)) + sb.WriteString(git.FormatCommitLine(commit)) fmt.Fprintf(&sb, "Root document: %s\n\n", rootFileName) sb.WriteString("## Directory Tree\n\n") diff --git a/internal/walker/walker.go b/internal/walker/walker.go index 4808aeb..5aec8c9 100644 --- a/internal/walker/walker.go +++ b/internal/walker/walker.go @@ -9,6 +9,7 @@ import ( "git.flo.fo/FLO/dirmd/internal/config" "git.flo.fo/FLO/dirmd/internal/filter" + "git.flo.fo/FLO/dirmd/internal/git" ) type Entry struct { @@ -21,6 +22,7 @@ type Result struct { AbsRoot string SingleFile bool OmitTest bool + Commit *git.CommitInfo } func Run(cfg *config.Config) (*Result, error) { @@ -53,11 +55,15 @@ func processSingleFile(cfg *config.Config) (*Result, error) { return nil, fmt.Errorf("file %s filtered out", cfg.AbsRoot) } + // For single files, try to get git info from the parent directory + commitInfo := git.GetCommitInfo(filepath.Dir(cfg.AbsRoot)) + return &Result{ Entries: []Entry{{RelPath: filepath.Base(cfg.AbsRoot), Size: info.Size()}}, AbsRoot: filepath.Dir(cfg.AbsRoot), SingleFile: true, OmitTest: cfg.OmitTest, + Commit: commitInfo, }, nil } @@ -73,6 +79,9 @@ func walkDirectory(cfg *config.Config) (*Result, error) { result := &Result{AbsRoot: cfg.AbsRoot, OmitTest: cfg.OmitTest} + // Fetch git info for the root directory + result.Commit = git.GetCommitInfo(cfg.AbsRoot) + err := filepath.WalkDir(cfg.AbsRoot, func(path string, d fs.DirEntry, err error) error { if err != nil { fmt.Fprintf(os.Stderr, "warning: %v\n", err)