A deployable markdown editor that connects with your self hosted files and lets you edit in a beautiful interface
at main 240 lines 6.5 kB view raw
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}