A deployable markdown editor that connects with your self hosted files and lets you edit in a beautiful interface
1package git
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "path/filepath"
8 "time"
9
10 "github.com/go-git/go-git/v5"
11 "github.com/go-git/go-git/v5/config"
12 "github.com/go-git/go-git/v5/plumbing"
13 "github.com/go-git/go-git/v5/plumbing/object"
14 "github.com/go-git/go-git/v5/plumbing/transport/http"
15)
16
17// GitOperations handles git operations on repositories
18type GitOperations struct {
19 cacheDir string
20}
21
22// NewGitOperations creates a new GitOperations instance
23func NewGitOperations(cacheDir string) *GitOperations {
24 return &GitOperations{
25 cacheDir: cacheDir,
26 }
27}
28
29// getRepoPath returns the local path for a repository
30func (g *GitOperations) getRepoPath(repoFullName string) string {
31 return filepath.Join(g.cacheDir, repoFullName)
32}
33
34// CloneOrOpen clones a repository if it doesn't exist, or opens it if it does
35func (g *GitOperations) CloneOrOpen(ctx context.Context, repoFullName, cloneURL, accessToken string) (*git.Repository, error) {
36 repoPath := g.getRepoPath(repoFullName)
37
38 // Check if repository already exists
39 if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil {
40 // Repository exists, open it
41 repo, err := git.PlainOpen(repoPath)
42 if err != nil {
43 return nil, fmt.Errorf("failed to open repository: %w", err)
44 }
45
46 // Fetch latest changes
47 err = repo.Fetch(&git.FetchOptions{
48 Auth: &http.BasicAuth{
49 Username: "oauth2",
50 Password: accessToken,
51 },
52 Force: true,
53 })
54 if err != nil && err != git.NoErrAlreadyUpToDate {
55 return nil, fmt.Errorf("failed to fetch: %w", err)
56 }
57
58 return repo, nil
59 }
60
61 // Repository doesn't exist, clone it
62 if err := os.MkdirAll(filepath.Dir(repoPath), 0755); err != nil {
63 return nil, fmt.Errorf("failed to create cache directory: %w", err)
64 }
65
66 repo, err := git.PlainClone(repoPath, false, &git.CloneOptions{
67 URL: cloneURL,
68 Auth: &http.BasicAuth{
69 Username: "oauth2",
70 Password: accessToken,
71 },
72 Progress: nil,
73 })
74 if err != nil {
75 return nil, fmt.Errorf("failed to clone repository: %w", err)
76 }
77
78 return repo, nil
79}
80
81// CreateBranch creates a new branch from the default branch
82func (g *GitOperations) CreateBranch(repo *git.Repository, branchName, baseBranch string) error {
83 // Get the base branch reference
84 baseBranchRef := plumbing.NewRemoteReferenceName("origin", baseBranch)
85 baseRef, err := repo.Reference(baseBranchRef, true)
86 if err != nil {
87 return fmt.Errorf("failed to get base branch reference: %w", err)
88 }
89
90 // Create new branch reference
91 newBranchRef := plumbing.NewBranchReferenceName(branchName)
92 ref := plumbing.NewHashReference(newBranchRef, baseRef.Hash())
93
94 // Store the reference
95 if err := repo.Storer.SetReference(ref); err != nil {
96 return fmt.Errorf("failed to create branch reference: %w", err)
97 }
98
99 // Checkout the new branch
100 w, err := repo.Worktree()
101 if err != nil {
102 return fmt.Errorf("failed to get worktree: %w", err)
103 }
104
105 if err := w.Checkout(&git.CheckoutOptions{
106 Branch: newBranchRef,
107 Force: true,
108 }); err != nil {
109 return fmt.Errorf("failed to checkout branch: %w", err)
110 }
111
112 return nil
113}
114
115// CheckoutBranch checks out an existing branch
116func (g *GitOperations) CheckoutBranch(repo *git.Repository, branchName string) error {
117 w, err := repo.Worktree()
118 if err != nil {
119 return fmt.Errorf("failed to get worktree: %w", err)
120 }
121
122 branchRef := plumbing.NewBranchReferenceName(branchName)
123 if err := w.Checkout(&git.CheckoutOptions{
124 Branch: branchRef,
125 Force: true,
126 }); err != nil {
127 return fmt.Errorf("failed to checkout branch: %w", err)
128 }
129
130 return nil
131}
132
133// WriteFile writes content to a file in the repository
134func (g *GitOperations) WriteFile(repoFullName, filePath, content string) error {
135 repoPath := g.getRepoPath(repoFullName)
136 fullPath := filepath.Join(repoPath, filePath)
137
138 // Create directory if it doesn't exist
139 if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
140 return fmt.Errorf("failed to create directory: %w", err)
141 }
142
143 // Write file
144 if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil {
145 return fmt.Errorf("failed to write file: %w", err)
146 }
147
148 return nil
149}
150
151// CommitChanges commits all changes with a message
152func (g *GitOperations) CommitChanges(repo *git.Repository, message string, filePaths []string) (string, error) {
153 w, err := repo.Worktree()
154 if err != nil {
155 return "", fmt.Errorf("failed to get worktree: %w", err)
156 }
157
158 // Add files
159 for _, filePath := range filePaths {
160 if _, err := w.Add(filePath); err != nil {
161 return "", fmt.Errorf("failed to add file %s: %w", filePath, err)
162 }
163 }
164
165 // Commit
166 commit, err := w.Commit(message, &git.CommitOptions{
167 Author: &object.Signature{
168 Name: "MarkEdit",
169 Email: "markedit@users.noreply.github.com",
170 When: time.Now(),
171 },
172 })
173 if err != nil {
174 return "", fmt.Errorf("failed to commit: %w", err)
175 }
176
177 return commit.String(), nil
178}
179
180// Push pushes the current branch to remote
181func (g *GitOperations) Push(repo *git.Repository, branchName, accessToken string) error {
182 // Get the branch reference
183 branchRef := plumbing.NewBranchReferenceName(branchName)
184
185 // Push the branch
186 err := repo.Push(&git.PushOptions{
187 RemoteName: "origin",
188 RefSpecs: []config.RefSpec{
189 config.RefSpec(branchRef + ":" + branchRef),
190 },
191 Auth: &http.BasicAuth{
192 Username: "oauth2",
193 Password: accessToken,
194 },
195 Force: false,
196 })
197
198 if err != nil && err != git.NoErrAlreadyUpToDate {
199 return fmt.Errorf("failed to push: %w", err)
200 }
201
202 return nil
203}
204
205// DeleteLocalBranch deletes a local branch
206func (g *GitOperations) DeleteLocalBranch(repo *git.Repository, branchName string) error {
207 branchRef := plumbing.NewBranchReferenceName(branchName)
208
209 if err := repo.Storer.RemoveReference(branchRef); err != nil {
210 return fmt.Errorf("failed to delete branch: %w", err)
211 }
212
213 return nil
214}
215
216// GetCurrentBranch returns the name of the current branch
217func (g *GitOperations) GetCurrentBranch(repo *git.Repository) (string, error) {
218 head, err := repo.Head()
219 if err != nil {
220 return "", fmt.Errorf("failed to get HEAD: %w", err)
221 }
222
223 if !head.Name().IsBranch() {
224 return "", fmt.Errorf("HEAD is not a branch")
225 }
226
227 return head.Name().Short(), nil
228}
229
230// DeleteFile removes a file from the repository worktree
231func (g *GitOperations) DeleteFile(repoFullName, filePath string) error {
232 repoPath := g.getRepoPath(repoFullName)
233 fullPath := filepath.Join(repoPath, filePath)
234
235 if err := os.Remove(fullPath); err != nil && !os.IsNotExist(err) {
236 return fmt.Errorf("failed to delete file: %w", err)
237 }
238
239 return nil
240}