1package main
2
3import (
4 "fmt"
5 "net/http"
6 "strings"
7
8 "github.com/go-git/go-git/v5"
9 "github.com/go-git/go-git/v5/plumbing"
10 "github.com/go-git/go-git/v5/plumbing/format/diff"
11 "github.com/go-git/go-git/v5/plumbing/object"
12)
13
14func commitHandler(w http.ResponseWriter, r *http.Request) {
15 ctx, err := getRepoContext(w, r)
16 if err != nil {
17 http.Error(w, err.Error(), http.StatusInternalServerError)
18 return
19 }
20
21 commitHash := r.PathValue("hash")
22 hash := plumbing.NewHash(commitHash)
23 commit, err := ctx.GitRepo.CommitObject(hash)
24 if err != nil {
25 http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
26 return
27 }
28
29 var fileDiffs []FileDiff
30 maxChanges := 0
31 totalAdds, totalDels := 0, 0
32 isCommitTooLarge := false
33
34 if commit.NumParents() > 0 {
35 parent, _ := commit.Parent(0)
36 patch, err := parent.Patch(commit)
37 if err == nil {
38 pStats := patch.Stats()
39 for _, s := range pStats {
40 totalAdds += s.Addition
41 totalDels += s.Deletion
42 }
43 isCommitTooLarge = (totalAdds + totalDels) > 5000
44
45 // Map stats by name for quick lookup
46 statsMap := make(map[string]object.FileStat)
47 for _, s := range pStats {
48 statsMap[s.Name] = s
49 }
50
51 for _, fp := range patch.FilePatches() {
52 from, to := fp.Files()
53 name := ""
54 mode := ""
55 if to != nil {
56 name = to.Path()
57 mode = formatMode(to.Mode())
58 } else if from != nil {
59 name = from.Path()
60 mode = formatMode(from.Mode())
61 }
62
63 stat := statsMap[name]
64 fileAdd, fileDel := stat.Addition, stat.Deletion
65 isBinary := fp.IsBinary()
66 deleted := to == nil
67 var oldSize, newSize int64
68
69 if isBinary {
70 if from != nil {
71 obj, _ := ctx.GitRepo.Object(plumbing.AnyObject, from.Hash())
72 if blob, ok := obj.(*object.Blob); ok {
73 oldSize = blob.Size
74 }
75 }
76 if to != nil {
77 obj, _ := ctx.GitRepo.Object(plumbing.AnyObject, to.Hash())
78 if blob, ok := obj.(*object.Blob); ok {
79 newSize = blob.Size
80 }
81 }
82 }
83
84 if fileAdd+fileDel > maxChanges {
85 maxChanges = fileAdd + fileDel
86 }
87
88 isFileTooLarge := (fileAdd + fileDel) > 1000
89 var filteredLines []DiffLine
90
91 if !isCommitTooLarge && !isFileTooLarge && !isBinary {
92 var diffLines []DiffLine
93 leftNo, rightNo := 1, 1
94
95 var delLines []string
96 var addLines []string
97
98 flush := func() {
99 max := len(delLines)
100 if len(addLines) > max {
101 max = len(addLines)
102 }
103 for i := 0; i < max; i++ {
104 line := DiffLine{}
105 if i < len(delLines) && i < len(addLines) {
106 line.LeftNo = fmt.Sprintf("%d", leftNo)
107 line.Left = delLines[i]
108 line.RightNo = fmt.Sprintf("%d", rightNo)
109 line.Right = addLines[i]
110 line.Type = "mod"
111 leftNo++
112 rightNo++
113 } else if i < len(delLines) {
114 line.LeftNo = fmt.Sprintf("%d", leftNo)
115 line.Left = delLines[i]
116 line.Type = "del"
117 leftNo++
118 } else if i < len(addLines) {
119 line.RightNo = fmt.Sprintf("%d", rightNo)
120 line.Right = addLines[i]
121 line.Type = "add"
122 rightNo++
123 }
124 diffLines = append(diffLines, line)
125 }
126 delLines = nil
127 addLines = nil
128 }
129
130 for _, chunk := range fp.Chunks() {
131 lines := strings.Split(strings.TrimSuffix(chunk.Content(), "\n"), "\n")
132 switch chunk.Type() {
133 case diff.Equal:
134 flush()
135 for _, line := range lines {
136 diffLines = append(diffLines, DiffLine{
137 LeftNo: fmt.Sprintf("%d", leftNo),
138 Left: line,
139 RightNo: fmt.Sprintf("%d", rightNo),
140 Right: line,
141 Type: "eq",
142 })
143 leftNo++
144 rightNo++
145 }
146 case diff.Delete:
147 delLines = append(delLines, lines...)
148 case diff.Add:
149 addLines = append(addLines, lines...)
150 }
151 }
152 flush()
153
154 visible := make([]bool, len(diffLines))
155 for i, line := range diffLines {
156 if line.Type == "add" || line.Type == "del" || line.Type == "mod" {
157 for j := i - 3; j <= i+3; j++ {
158 if j >= 0 && j < len(diffLines) {
159 visible[j] = true
160 }
161 }
162 }
163 }
164
165 lastWasGap := false
166 for i, isVisible := range visible {
167 if isVisible {
168 filteredLines = append(filteredLines, diffLines[i])
169 lastWasGap = false
170 } else {
171 if !lastWasGap {
172 filteredLines = append(filteredLines, DiffLine{Type: "gap"})
173 lastWasGap = true
174 }
175 }
176 }
177 }
178
179 fileDiffs = append(fileDiffs, FileDiff{
180 Name: name,
181 Lines: filteredLines,
182 Addition: fileAdd,
183 Deletion: fileDel,
184 IsBinary: isBinary,
185 Mode: mode,
186 OldSize: oldSize,
187 NewSize: newSize,
188 Deleted: deleted,
189 TooLarge: isFileTooLarge,
190 })
191 }
192 }
193 } else {
194 // Initial commit - no parents
195 // We could still show the whole file as additions, but the current logic handles parents only
196 }
197
198 data := struct {
199 *RepoContext
200 Commit Commit
201 FileDiffs []FileDiff
202 MaxChanges int
203 }{
204 RepoContext: ctx,
205 Commit: Commit{
206 Hash: commit.Hash.String(),
207 AuthorName: commit.Author.Name,
208 AuthorEmail: commit.Author.Email,
209 AuthorDate: commit.Author.When,
210 CommitterName: commit.Committer.Name,
211 CommitterEmail: commit.Committer.Email,
212 CommitterDate: commit.Committer.When,
213 Message: commit.Message,
214 Additions: totalAdds,
215 Deletions: totalDels,
216 TooLarge: isCommitTooLarge,
217 },
218 FileDiffs: fileDiffs,
219 MaxChanges: maxChanges,
220 }
221
222 err = templates.ExecuteTemplate(w, "commit.html", data)
223 if err != nil {
224 http.Error(w, err.Error(), http.StatusInternalServerError)
225 }
226}
227
228func patchHandler(w http.ResponseWriter, r *http.Request) {
229 repoName := r.PathValue("name")
230 commitHash := r.PathValue("hash")
231
232 config, err := loadConfig(ConfigPath)
233 if err != nil {
234 http.Error(w, fmt.Sprintf("Error loading config: %v", err), http.StatusInternalServerError)
235 return
236 }
237
238 var repo *Repository
239 for _, repoItem := range config.Repositories {
240 if repoItem.Name == repoName {
241 repo = &repoItem
242 break
243 }
244 }
245
246 if repo == nil {
247 http.NotFound(w, r)
248 return
249 }
250
251 gitRepo, err := git.PlainOpen(repo.Path)
252 if err != nil {
253 http.Error(w, fmt.Sprintf("Error opening repository: %v", err), http.StatusInternalServerError)
254 return
255 }
256
257 hash := plumbing.NewHash(commitHash)
258 commit, err := gitRepo.CommitObject(hash)
259 if err != nil {
260 http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
261 return
262 }
263
264 w.Header().Set("Content-Type", "text/plain")
265
266 currentTree, err := commit.Tree()
267 if err != nil {
268 http.Error(w, fmt.Sprintf("Error getting tree: %v", err), http.StatusInternalServerError)
269 return
270 }
271
272 var parentTree *object.Tree
273 if commit.NumParents() > 0 {
274 parent, _ := commit.Parent(0)
275 parentTree, err = parent.Tree()
276 if err != nil {
277 http.Error(w, fmt.Sprintf("Error getting parent tree: %v", err), http.StatusInternalServerError)
278 return
279 }
280 }
281
282 patch, err := parentTree.Patch(currentTree)
283 if err != nil {
284 http.Error(w, fmt.Sprintf("Error generating patch: %v", err), http.StatusInternalServerError)
285 return
286 }
287 fmt.Fprint(w, patch.String())
288}