changelog generator & diff tool
stormlightlabs.github.io/git-storm/
changelog
changeset
markdown
golang
git
1package diff
2
3import "io"
4
5// DiffViewKind enumerates the supported diff views.
6type DiffViewKind int
7
8const (
9 ViewUnified DiffViewKind = iota
10 ViewSplit
11 ViewHunk
12 ViewInline
13 ViewRich
14 ViewSource
15)
16
17// String returns a human-readable name for the view kind.
18func (k DiffViewKind) String() string {
19 switch k {
20 case ViewUnified:
21 return "Unified"
22 case ViewSplit:
23 return "Split"
24 case ViewHunk:
25 return "Hunk"
26 case ViewInline:
27 return "Inline"
28 case ViewRich:
29 return "Rich"
30 case ViewSource:
31 return "Source"
32 default:
33 return "Unknown"
34 }
35}
36
37// DiffResult holds the output of a diff tool.
38type DiffResult struct {
39 // Content is the rendered diff output (text, ANSI, HTML, etc.)
40 Content string
41
42 // View is the kind of view used to render the diff.
43 View DiffViewKind
44
45 // Metadata like file paths, commit hashes, timestamps (optional).
46 // You may extend as needed.
47 OldPath string
48 NewPath string
49 FromHash string
50 ToHash string
51}
52
53// DiffTool is the interface for generating diffs between two versions.
54type DiffTool interface {
55 // Diff takes two blobs (old and new versions) and returns a DiffResult.
56 // The reader parameters may be full file contents or other abstraction.
57 // The viewKind parameter selects which view implementation (Unified/Split/...).
58 Diff(oldContent io.Reader, newContent io.Reader, viewKind DiffViewKind) (DiffResult, error)
59}
60
61// UnifiedDiff implements unified view (single linear view with additions & deletions).
62//
63// TODO: Support pluggable diff algorithms beyond Myers.
64type UnifiedDiff struct {
65 // TerminalWidth is the total available width for rendering
66 TerminalWidth int
67 // ShowLineNumbers controls whether line numbers are displayed
68 ShowLineNumbers bool
69 // Expanded controls whether to show all unchanged lines or compress them
70 Expanded bool
71 // EnableWordWrap enables word wrapping for long lines
72 EnableWordWrap bool
73}
74
75// Diff generates a unified diff view from two content readers.
76func (u *UnifiedDiff) Diff(oldContent io.Reader, newContent io.Reader, viewKind DiffViewKind) (DiffResult, error) {
77 oldBytes, err := io.ReadAll(oldContent)
78 if err != nil {
79 return DiffResult{}, err
80 }
81 newBytes, err := io.ReadAll(newContent)
82 if err != nil {
83 return DiffResult{}, err
84 }
85
86 oldLines := splitLines(string(oldBytes))
87 newLines := splitLines(string(newBytes))
88
89 myers := &Myers{}
90 edits, err := myers.Compute(oldLines, newLines)
91 if err != nil {
92 return DiffResult{}, err
93 }
94
95 formatter := &UnifiedFormatter{
96 TerminalWidth: u.TerminalWidth,
97 ShowLineNumbers: u.ShowLineNumbers,
98 Expanded: u.Expanded,
99 EnableWordWrap: u.EnableWordWrap,
100 }
101
102 content := formatter.Format(edits)
103
104 return DiffResult{
105 Content: content,
106 View: ViewUnified,
107 }, nil
108}
109
110// SplitDiff implements side-by-side view (old on left, new on right).
111//
112// TODO: Support pluggable diff algorithms beyond Myers.
113type SplitDiff struct {
114 // TerminalWidth is the total available width for rendering
115 TerminalWidth int
116 // ShowLineNumbers controls whether line numbers are displayed
117 ShowLineNumbers bool
118 // Expanded controls whether to show all unchanged lines or compress them
119 Expanded bool
120 // EnableWordWrap enables word wrapping for long lines
121 EnableWordWrap bool
122}
123
124// Diff generates a side-by-side diff view from two content readers.
125func (s *SplitDiff) Diff(oldContent io.Reader, newContent io.Reader, viewKind DiffViewKind) (DiffResult, error) {
126 oldBytes, err := io.ReadAll(oldContent)
127 if err != nil {
128 return DiffResult{}, err
129 }
130 newBytes, err := io.ReadAll(newContent)
131 if err != nil {
132 return DiffResult{}, err
133 }
134
135 oldLines := splitLines(string(oldBytes))
136 newLines := splitLines(string(newBytes))
137
138 myers := &Myers{}
139 edits, err := myers.Compute(oldLines, newLines)
140 if err != nil {
141 return DiffResult{}, err
142 }
143
144 formatter := &SideBySideFormatter{
145 TerminalWidth: s.TerminalWidth,
146 ShowLineNumbers: s.ShowLineNumbers,
147 Expanded: s.Expanded,
148 EnableWordWrap: s.EnableWordWrap,
149 }
150
151 content := formatter.Format(edits)
152
153 return DiffResult{
154 Content: content,
155 View: ViewSplit,
156 }, nil
157}
158
159// splitLines splits a string into lines, preserving empty lines.
160func splitLines(s string) []string {
161 if s == "" {
162 return []string{}
163 }
164 lines := make([]string, 0)
165 start := 0
166 for i := 0; i < len(s); i++ {
167 if s[i] == '\n' {
168 lines = append(lines, s[start:i])
169 start = i + 1
170 }
171 }
172
173 if start < len(s) {
174 lines = append(lines, s[start:])
175 }
176 return lines
177}
178
179// HunkDiff focuses on changed blocks, minimal context.
180type HunkDiff struct{}
181
182// InlineDiff renders changes inline in full file flow.
183type InlineDiff struct{}
184
185// RichDiff renders changes for formatted preview (for example, markdown or html previews).
186type RichDiff struct{}
187
188// SourceDiff simply returns raw diff data or patch format without special formatting.
189type SourceDiff struct{}