main
1package git
2
3import (
4 "fmt"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "strconv"
9 "strings"
10
11 "github.com/xlgmokha/mcp/pkg/mcp"
12)
13
14// Commit represents a git commit
15type Commit struct {
16 Hash string
17 Message string
18 Author string
19 Date string
20}
21
22// GitOperations provides git operations for a specific repository
23type GitOperations struct {
24 repoPath string
25}
26
27// NewGitOperations creates a new GitOperations helper
28func NewGitOperations(repoPath string) *GitOperations {
29 return &GitOperations{repoPath: repoPath}
30}
31
32// New creates a new Git MCP server
33func New(repoPath string) *mcp.Server {
34 git := NewGitOperations(repoPath)
35 builder := mcp.NewServerBuilder("mcp-git", "1.0.0")
36
37 // Add git_status tool
38 builder.AddTool(mcp.NewTool("git_status", "Shows the working tree status", map[string]interface{}{
39 "type": "object",
40 "properties": map[string]interface{}{
41 "repo_path": map[string]interface{}{
42 "type": "string",
43 "description": "Path to the Git repository",
44 },
45 },
46 "required": []string{"repo_path"},
47 }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
48 repoPath, ok := req.Arguments["repo_path"].(string)
49 if !ok {
50 repoPath = git.repoPath
51 }
52
53 output, err := git.runGitCommand(repoPath, "status")
54 if err != nil {
55 return mcp.NewToolError(fmt.Sprintf("git status failed: %v", err)), nil
56 }
57
58 return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Repository status:\n%s", output))), nil
59 }))
60
61 // Add git_diff_unstaged tool
62 builder.AddTool(mcp.NewTool("git_diff_unstaged", "Shows changes in the working directory that are not yet staged", map[string]interface{}{
63 "type": "object",
64 "properties": map[string]interface{}{
65 "repo_path": map[string]interface{}{
66 "type": "string",
67 "description": "Path to the Git repository",
68 },
69 },
70 "required": []string{"repo_path"},
71 }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
72 repoPath, ok := req.Arguments["repo_path"].(string)
73 if !ok {
74 repoPath = git.repoPath
75 }
76
77 output, err := git.runGitCommand(repoPath, "diff")
78 if err != nil {
79 return mcp.NewToolError(fmt.Sprintf("git diff failed: %v", err)), nil
80 }
81
82 return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Unstaged changes:\n%s", output))), nil
83 }))
84
85 // Add git_diff_staged tool
86 builder.AddTool(mcp.NewTool("git_diff_staged", "Shows changes that are staged for commit", map[string]interface{}{
87 "type": "object",
88 "properties": map[string]interface{}{
89 "repo_path": map[string]interface{}{
90 "type": "string",
91 "description": "Path to the Git repository",
92 },
93 },
94 "required": []string{"repo_path"},
95 }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
96 repoPath, ok := req.Arguments["repo_path"].(string)
97 if !ok {
98 repoPath = git.repoPath
99 }
100
101 output, err := git.runGitCommand(repoPath, "diff", "--cached")
102 if err != nil {
103 return mcp.NewToolError(fmt.Sprintf("git diff --cached failed: %v", err)), nil
104 }
105
106 return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Staged changes:\n%s", output))), nil
107 }))
108
109 // Add git_diff tool
110 builder.AddTool(mcp.NewTool("git_diff", "Shows differences between branches or commits", map[string]interface{}{
111 "type": "object",
112 "properties": map[string]interface{}{
113 "repo_path": map[string]interface{}{
114 "type": "string",
115 "description": "Path to the Git repository",
116 },
117 "target": map[string]interface{}{
118 "type": "string",
119 "description": "Target branch or commit to diff against",
120 },
121 },
122 "required": []string{"repo_path", "target"},
123 }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
124 repoPath, ok := req.Arguments["repo_path"].(string)
125 if !ok {
126 repoPath = git.repoPath
127 }
128
129 target, ok := req.Arguments["target"].(string)
130 if !ok {
131 return mcp.NewToolError("target is required"), nil
132 }
133
134 output, err := git.runGitCommand(repoPath, "diff", target)
135 if err != nil {
136 return mcp.NewToolError(fmt.Sprintf("git diff failed: %v", err)), nil
137 }
138
139 return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Diff with %s:\n%s", target, output))), nil
140 }))
141
142 // Add git_commit tool
143 builder.AddTool(mcp.NewTool("git_commit", "Records changes to the repository", map[string]interface{}{
144 "type": "object",
145 "properties": map[string]interface{}{
146 "repo_path": map[string]interface{}{
147 "type": "string",
148 "description": "Path to the Git repository",
149 },
150 "message": map[string]interface{}{
151 "type": "string",
152 "description": "Commit message",
153 },
154 },
155 "required": []string{"repo_path", "message"},
156 }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
157 repoPath, ok := req.Arguments["repo_path"].(string)
158 if !ok {
159 repoPath = git.repoPath
160 }
161
162 message, ok := req.Arguments["message"].(string)
163 if !ok {
164 return mcp.NewToolError("message is required"), nil
165 }
166
167 output, err := git.runGitCommand(repoPath, "commit", "-m", message)
168 if err != nil {
169 return mcp.NewToolError(fmt.Sprintf("git commit failed: %v", err)), nil
170 }
171
172 return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Changes committed successfully:\n%s", output))), nil
173 }))
174
175 // Add git_add tool
176 builder.AddTool(mcp.NewTool("git_add", "Adds file contents to the staging area", map[string]interface{}{
177 "type": "object",
178 "properties": map[string]interface{}{
179 "repo_path": map[string]interface{}{
180 "type": "string",
181 "description": "Path to the Git repository",
182 },
183 "files": map[string]interface{}{
184 "type": "array",
185 "items": map[string]interface{}{
186 "type": "string",
187 },
188 "description": "List of files to add",
189 },
190 },
191 "required": []string{"repo_path", "files"},
192 }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
193 repoPath, ok := req.Arguments["repo_path"].(string)
194 if !ok {
195 repoPath = git.repoPath
196 }
197
198 filesInterface, ok := req.Arguments["files"]
199 if !ok {
200 return mcp.NewToolError("files is required"), nil
201 }
202
203 files, err := convertToStringSlice(filesInterface)
204 if err != nil {
205 return mcp.NewToolError("files must be an array of strings"), nil
206 }
207
208 args := append([]string{"add"}, files...)
209 _, err = git.runGitCommand(repoPath, args...)
210 if err != nil {
211 return mcp.NewToolError(fmt.Sprintf("git add failed: %v", err)), nil
212 }
213
214 return mcp.NewToolResult(mcp.NewTextContent("Files staged successfully")), nil
215 }))
216
217 // Add git_reset tool
218 builder.AddTool(mcp.NewTool("git_reset", "Unstages all staged changes", map[string]interface{}{
219 "type": "object",
220 "properties": map[string]interface{}{
221 "repo_path": map[string]interface{}{
222 "type": "string",
223 "description": "Path to the Git repository",
224 },
225 },
226 "required": []string{"repo_path"},
227 }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
228 repoPath, ok := req.Arguments["repo_path"].(string)
229 if !ok {
230 repoPath = git.repoPath
231 }
232
233 _, err := git.runGitCommand(repoPath, "reset")
234 if err != nil {
235 return mcp.NewToolError(fmt.Sprintf("git reset failed: %v", err)), nil
236 }
237
238 return mcp.NewToolResult(mcp.NewTextContent("All staged changes reset")), nil
239 }))
240
241 // Add git_log tool
242 builder.AddTool(mcp.NewTool("git_log", "Shows the commit logs", map[string]interface{}{
243 "type": "object",
244 "properties": map[string]interface{}{
245 "repo_path": map[string]interface{}{
246 "type": "string",
247 "description": "Path to the Git repository",
248 },
249 "max_count": map[string]interface{}{
250 "type": "integer",
251 "description": "Maximum number of commits to show",
252 "default": 10,
253 },
254 },
255 "required": []string{"repo_path"},
256 }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
257 repoPath, ok := req.Arguments["repo_path"].(string)
258 if !ok {
259 repoPath = git.repoPath
260 }
261
262 maxCount := 10
263 if mc, exists := req.Arguments["max_count"]; exists {
264 if count, ok := mc.(float64); ok {
265 maxCount = int(count)
266 }
267 }
268
269 output, err := git.runGitCommand(repoPath, "log", "--oneline", "-n", strconv.Itoa(maxCount))
270 if err != nil {
271 return mcp.NewToolError(fmt.Sprintf("git log failed: %v", err)), nil
272 }
273
274 return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Commit history:\n%s", output))), nil
275 }))
276
277 // Add git_create_branch tool
278 builder.AddTool(mcp.NewTool("git_create_branch", "Creates a new branch from an optional base branch", map[string]interface{}{
279 "type": "object",
280 "properties": map[string]interface{}{
281 "repo_path": map[string]interface{}{
282 "type": "string",
283 "description": "Path to the Git repository",
284 },
285 "branch_name": map[string]interface{}{
286 "type": "string",
287 "description": "Name of the new branch",
288 },
289 "base_branch": map[string]interface{}{
290 "type": "string",
291 "description": "Base branch to create from (optional)",
292 },
293 },
294 "required": []string{"repo_path", "branch_name"},
295 }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
296 repoPath, ok := req.Arguments["repo_path"].(string)
297 if !ok {
298 repoPath = git.repoPath
299 }
300
301 branchName, ok := req.Arguments["branch_name"].(string)
302 if !ok {
303 return mcp.NewToolError("branch_name is required"), nil
304 }
305
306 baseBranch, _ := req.Arguments["base_branch"].(string)
307
308 var args []string
309 if baseBranch != "" {
310 args = []string{"checkout", "-b", branchName, baseBranch}
311 } else {
312 args = []string{"checkout", "-b", branchName}
313 }
314
315 _, err := git.runGitCommand(repoPath, args...)
316 if err != nil {
317 return mcp.NewToolError(fmt.Sprintf("git create branch failed: %v", err)), nil
318 }
319
320 return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Created branch '%s'", branchName))), nil
321 }))
322
323 // Add git_checkout tool
324 builder.AddTool(mcp.NewTool("git_checkout", "Switches branches", map[string]interface{}{
325 "type": "object",
326 "properties": map[string]interface{}{
327 "repo_path": map[string]interface{}{
328 "type": "string",
329 "description": "Path to the Git repository",
330 },
331 "branch_name": map[string]interface{}{
332 "type": "string",
333 "description": "Name of the branch to checkout",
334 },
335 },
336 "required": []string{"repo_path", "branch_name"},
337 }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
338 repoPath, ok := req.Arguments["repo_path"].(string)
339 if !ok {
340 repoPath = git.repoPath
341 }
342
343 branchName, ok := req.Arguments["branch_name"].(string)
344 if !ok {
345 return mcp.NewToolError("branch_name is required"), nil
346 }
347
348 _, err := git.runGitCommand(repoPath, "checkout", branchName)
349 if err != nil {
350 return mcp.NewToolError(fmt.Sprintf("git checkout failed: %v", err)), nil
351 }
352
353 return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Switched to branch '%s'", branchName))), nil
354 }))
355
356 // Add git_show tool
357 builder.AddTool(mcp.NewTool("git_show", "Shows the contents of a commit", map[string]interface{}{
358 "type": "object",
359 "properties": map[string]interface{}{
360 "repo_path": map[string]interface{}{
361 "type": "string",
362 "description": "Path to the Git repository",
363 },
364 "revision": map[string]interface{}{
365 "type": "string",
366 "description": "Commit hash or reference to show",
367 },
368 },
369 "required": []string{"repo_path", "revision"},
370 }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
371 repoPath, ok := req.Arguments["repo_path"].(string)
372 if !ok {
373 repoPath = git.repoPath
374 }
375
376 revision, ok := req.Arguments["revision"].(string)
377 if !ok {
378 return mcp.NewToolError("revision is required"), nil
379 }
380
381 output, err := git.runGitCommand(repoPath, "show", revision)
382 if err != nil {
383 return mcp.NewToolError(fmt.Sprintf("git show failed: %v", err)), nil
384 }
385
386 return mcp.NewToolResult(mcp.NewTextContent(output)), nil
387 }))
388
389 // Add git_init tool
390 builder.AddTool(mcp.NewTool("git_init", "Initialize a new Git repository", map[string]interface{}{
391 "type": "object",
392 "properties": map[string]interface{}{
393 "repo_path": map[string]interface{}{
394 "type": "string",
395 "description": "Path where to initialize the Git repository",
396 },
397 },
398 "required": []string{"repo_path"},
399 }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
400 repoPath, ok := req.Arguments["repo_path"].(string)
401 if !ok {
402 repoPath = git.repoPath
403 }
404
405 if err := os.MkdirAll(repoPath, 0755); err != nil {
406 return mcp.NewToolError(fmt.Sprintf("failed to create directory: %v", err)), nil
407 }
408
409 _, err := git.runGitCommand(repoPath, "init")
410 if err != nil {
411 return mcp.NewToolError(fmt.Sprintf("git init failed: %v", err)), nil
412 }
413
414 return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Initialized empty Git repository in %s", repoPath))), nil
415 }))
416
417 // Add commit-message prompt
418 builder.AddPrompt(mcp.NewPrompt("commit-message", "Prompt for crafting a well-structured git commit message", []mcp.PromptArgument{
419 {
420 Name: "changes",
421 Description: "Description of the changes being committed",
422 Required: true,
423 },
424 {
425 Name: "type",
426 Description: "Type of change (feat, fix, docs, style, refactor, test, chore)",
427 Required: false,
428 },
429 {
430 Name: "breaking",
431 Description: "Whether this is a breaking change (true/false)",
432 Required: false,
433 },
434 }, func(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
435 changes, hasChanges := req.Arguments["changes"].(string)
436 commitType, hasType := req.Arguments["type"].(string)
437 breaking, hasBreaking := req.Arguments["breaking"]
438
439 if !hasChanges || changes == "" {
440 return mcp.GetPromptResult{}, fmt.Errorf("changes argument is required")
441 }
442
443 if !hasType || commitType == "" {
444 commitType = "feat"
445 }
446
447 isBreaking := false
448 if hasBreaking {
449 if breakingBool, ok := breaking.(bool); ok {
450 isBreaking = breakingBool
451 } else if breakingStr, ok := breaking.(string); ok {
452 isBreaking = breakingStr == "true"
453 }
454 }
455
456 var messages []mcp.PromptMessage
457
458 userContent := fmt.Sprintf(`I need help writing a git commit message for the following changes:
459
460%s
461
462Please help me craft a well-structured commit message following conventional commit format.`, changes)
463
464 if hasType {
465 userContent += fmt.Sprintf("\n\nCommit type: %s", commitType)
466 }
467 if isBreaking {
468 userContent += "\n\nThis is a BREAKING CHANGE."
469 }
470
471 messages = append(messages, mcp.PromptMessage{
472 Role: "user",
473 Content: mcp.NewTextContent(userContent),
474 })
475
476 breakingPrefix := ""
477 if isBreaking {
478 breakingPrefix = "!"
479 }
480
481 assistantContent := fmt.Sprintf(`I'll help you create a conventional commit message. Here's the suggested format:
482
483**Commit message:**
484%s%s: %s
485
486**Format explanation:**
487- Type: %s (indicates the nature of the change)
488- Description: Clear, concise summary in present tense
489%s
490
491**Additional guidelines:**
492- Keep the subject line under 50 characters
493- Use imperative mood ("add" not "added")
494- Don't end subject line with a period
495- Include body if needed to explain what and why`,
496 commitType, breakingPrefix, changes,
497 commitType,
498 func() string {
499 if isBreaking {
500 return "- Breaking change: This change breaks backward compatibility"
501 }
502 return ""
503 }())
504
505 messages = append(messages, mcp.PromptMessage{
506 Role: "assistant",
507 Content: mcp.NewTextContent(assistantContent),
508 })
509
510 description := fmt.Sprintf("Commit message guidance for %s changes", commitType)
511 if isBreaking {
512 description += " (BREAKING)"
513 }
514
515 return mcp.GetPromptResult{
516 Description: description,
517 Messages: messages,
518 }, nil
519 }))
520
521 // Add git:// pattern resource for dynamic file access
522 builder.AddResource(mcp.NewResource(
523 "git://",
524 "Git Repository",
525 "",
526 func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
527 return git.handleGitResource(req)
528 },
529 ))
530
531 // Add repository root if it's a git repository
532 gitDir := filepath.Join(git.repoPath, ".git")
533 if _, err := os.Stat(gitDir); err == nil {
534 currentBranch, err := git.getCurrentBranch()
535 if err != nil {
536 currentBranch = "unknown"
537 }
538
539 gitURI := "git://" + git.repoPath
540 repoName := filepath.Base(git.repoPath)
541 if repoName == "." || repoName == "/" {
542 repoName = git.repoPath
543 }
544
545 rootName := fmt.Sprintf("Git Repository: %s (branch: %s)", repoName, currentBranch)
546 builder.AddRoot(mcp.NewRoot(gitURI, rootName))
547 }
548
549 return builder.Build()
550}
551
552// Helper methods for GitOperations
553
554func (git *GitOperations) runGitCommand(repoPath string, args ...string) (string, error) {
555 if _, err := os.Stat(repoPath); os.IsNotExist(err) {
556 return "", fmt.Errorf("repository path does not exist: %s", repoPath)
557 }
558
559 if len(args) > 0 && args[0] != "init" {
560 gitDir := filepath.Join(repoPath, ".git")
561 if _, err := os.Stat(gitDir); os.IsNotExist(err) {
562 return "", fmt.Errorf("not a git repository: %s", repoPath)
563 }
564 }
565
566 cmd := exec.Command("git", args...)
567 cmd.Dir = repoPath
568
569 output, err := cmd.CombinedOutput()
570 if err != nil {
571 return "", fmt.Errorf("%v: %s", err, string(output))
572 }
573
574 return strings.TrimSpace(string(output)), nil
575}
576
577func (git *GitOperations) getCurrentBranch() (string, error) {
578 output, err := git.runGitCommand(git.repoPath, "branch", "--show-current")
579 if err != nil {
580 return "", err
581 }
582 branch := strings.TrimSpace(output)
583 if branch == "" {
584 branch = "HEAD"
585 }
586 return branch, nil
587}
588
589func (git *GitOperations) getTrackedFiles() ([]string, error) {
590 output, err := git.runGitCommand(git.repoPath, "ls-files")
591 if err != nil {
592 return nil, err
593 }
594
595 if output == "" {
596 return []string{}, nil
597 }
598
599 files := strings.Split(output, "\n")
600 var filteredFiles []string
601
602 for _, file := range files {
603 file = strings.TrimSpace(file)
604 if file != "" && !strings.HasPrefix(file, ".") {
605 filteredFiles = append(filteredFiles, file)
606 }
607 }
608
609 return filteredFiles, nil
610}
611
612func (git *GitOperations) getBranches() ([]string, error) {
613 output, err := git.runGitCommand(git.repoPath, "branch", "--format=%(refname:short)")
614 if err != nil {
615 return nil, err
616 }
617
618 if output == "" {
619 return []string{}, nil
620 }
621
622 branches := strings.Split(output, "\n")
623 var filteredBranches []string
624
625 for _, branch := range branches {
626 branch = strings.TrimSpace(branch)
627 if branch != "" {
628 filteredBranches = append(filteredBranches, branch)
629 }
630 }
631
632 return filteredBranches, nil
633}
634
635func (git *GitOperations) getRecentCommits(count int) ([]Commit, error) {
636 output, err := git.runGitCommand(git.repoPath, "log", "--format=%H|%s|%an|%ad", "--date=short", "-n", strconv.Itoa(count))
637 if err != nil {
638 return nil, err
639 }
640
641 if output == "" {
642 return []Commit{}, nil
643 }
644
645 lines := strings.Split(output, "\n")
646 var commits []Commit
647
648 for _, line := range lines {
649 line = strings.TrimSpace(line)
650 if line == "" {
651 continue
652 }
653
654 parts := strings.Split(line, "|")
655 if len(parts) >= 4 {
656 commits = append(commits, Commit{
657 Hash: parts[0],
658 Message: parts[1],
659 Author: parts[2],
660 Date: parts[3],
661 })
662 }
663 }
664
665 return commits, nil
666}
667
668func (git *GitOperations) handleGitResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
669 if !strings.HasPrefix(req.URI, "git://") {
670 return mcp.ReadResourceResult{}, fmt.Errorf("invalid git URI: %s", req.URI)
671 }
672
673 uriPath := req.URI[6:]
674 parts := strings.Split(uriPath, "/")
675
676 if len(parts) < 3 {
677 return mcp.ReadResourceResult{}, fmt.Errorf("invalid git URI format: %s", req.URI)
678 }
679
680 repoPath := parts[0]
681 resourceType := parts[1]
682 resourcePath := strings.Join(parts[2:], "/")
683
684 if repoPath != git.repoPath {
685 return mcp.ReadResourceResult{}, fmt.Errorf("access denied: repository path mismatch")
686 }
687
688 switch resourceType {
689 case "branch":
690 return git.handleBranchResource(resourcePath)
691 case "commit":
692 return git.handleCommitResource(resourcePath)
693 default:
694 return git.handleFileResource(resourceType, resourcePath)
695 }
696}
697
698func (git *GitOperations) handleFileResource(branch, filePath string) (mcp.ReadResourceResult, error) {
699 gitPath := fmt.Sprintf("%s:%s", branch, filePath)
700 output, err := git.runGitCommand(git.repoPath, "show", gitPath)
701 if err != nil {
702 return mcp.ReadResourceResult{}, fmt.Errorf("failed to read git file: %v", err)
703 }
704
705 return mcp.ReadResourceResult{
706 Contents: []mcp.Content{
707 mcp.NewTextContent(output),
708 },
709 }, nil
710}
711
712func (git *GitOperations) handleBranchResource(branchName string) (mcp.ReadResourceResult, error) {
713 output, err := git.runGitCommand(git.repoPath, "log", "--oneline", "-n", "5", branchName)
714 if err != nil {
715 return mcp.ReadResourceResult{}, fmt.Errorf("failed to get branch info: %v", err)
716 }
717
718 content := fmt.Sprintf("Branch: %s\n\nRecent commits:\n%s", branchName, output)
719
720 return mcp.ReadResourceResult{
721 Contents: []mcp.Content{
722 mcp.NewTextContent(content),
723 },
724 }, nil
725}
726
727func (git *GitOperations) handleCommitResource(commitHash string) (mcp.ReadResourceResult, error) {
728 output, err := git.runGitCommand(git.repoPath, "show", "--stat", commitHash)
729 if err != nil {
730 return mcp.ReadResourceResult{}, fmt.Errorf("failed to get commit info: %v", err)
731 }
732
733 return mcp.ReadResourceResult{
734 Contents: []mcp.Content{
735 mcp.NewTextContent(output),
736 },
737 }, nil
738}
739
740// Helper function to convert interface{} to []string
741func convertToStringSlice(input interface{}) ([]string, error) {
742 switch v := input.(type) {
743 case []interface{}:
744 result := make([]string, len(v))
745 for i, item := range v {
746 str, ok := item.(string)
747 if !ok {
748 return nil, fmt.Errorf("item at index %d is not a string", i)
749 }
750 result[i] = str
751 }
752 return result, nil
753 case []string:
754 return v, nil
755 default:
756 return nil, fmt.Errorf("input is not a slice")
757 }
758}
759
760// Helper function to determine MIME type for git files
761func getGitMimeType(filePath string) string {
762 ext := strings.ToLower(filepath.Ext(filePath))
763 switch ext {
764 case ".go":
765 return "text/x-go"
766 case ".js":
767 return "text/javascript"
768 case ".ts":
769 return "text/typescript"
770 case ".py":
771 return "text/x-python"
772 case ".md":
773 return "text/markdown"
774 case ".json":
775 return "application/json"
776 case ".yaml", ".yml":
777 return "application/x-yaml"
778 case ".xml":
779 return "application/xml"
780 case ".html", ".htm":
781 return "text/html"
782 case ".css":
783 return "text/css"
784 case ".txt":
785 return "text/plain"
786 default:
787 return "text/plain"
788 }
789}