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}