| iomodo | 5a7e4e7 | 2025-07-25 13:21:41 +0400 | [diff] [blame] | 1 | # Git Interface for Go |
| 2 | |
| 3 | A comprehensive Go interface for Git operations that provides a clean, type-safe API for interacting with Git repositories. |
| 4 | |
| 5 | ## Features |
| 6 | |
| 7 | - **Complete Git Operations**: Supports all major Git operations including repository management, commits, branches, remotes, tags, and more |
| 8 | - **Type-Safe API**: Strongly typed interfaces and structs for better code safety |
| 9 | - **Context Support**: All operations support context for cancellation and timeouts |
| 10 | - **Error Handling**: Comprehensive error handling with detailed error messages |
| 11 | - **Flexible Configuration**: Configurable timeouts and environment variables |
| 12 | - **Easy to Use**: Simple and intuitive API design |
| 13 | |
| 14 | ## Installation |
| 15 | |
| 16 | The Git interface is part of the `github.com/iomodo/staff` module. No additional dependencies are required beyond the standard library. |
| 17 | |
| 18 | ## Quick Start |
| 19 | |
| 20 | ```go |
| 21 | package main |
| 22 | |
| 23 | import ( |
| 24 | "context" |
| 25 | "fmt" |
| 26 | "log" |
| 27 | |
| 28 | "github.com/iomodo/staff/git" |
| 29 | ) |
| 30 | |
| 31 | func main() { |
| 32 | ctx := context.Background() |
| 33 | |
| 34 | // Create a Git instance |
| 35 | git := git.DefaultGit("/path/to/your/repo") |
| 36 | |
| 37 | // Open an existing repository |
| 38 | if err := git.Open(ctx, "/path/to/your/repo"); err != nil { |
| 39 | log.Fatalf("Failed to open repository: %v", err) |
| 40 | } |
| 41 | |
| 42 | // Get repository status |
| 43 | status, err := git.Status(ctx) |
| 44 | if err != nil { |
| 45 | log.Fatalf("Failed to get status: %v", err) |
| 46 | } |
| 47 | |
| 48 | fmt.Printf("Current branch: %s\n", status.Branch) |
| 49 | fmt.Printf("Repository is clean: %t\n", status.IsClean) |
| 50 | } |
| 51 | ``` |
| 52 | |
| 53 | ## Core Interface |
| 54 | |
| 55 | ### GitInterface |
| 56 | |
| 57 | The main interface that defines all Git operations: |
| 58 | |
| 59 | ```go |
| 60 | type GitInterface interface { |
| 61 | // Repository operations |
| 62 | Init(ctx context.Context, path string) error |
| 63 | Clone(ctx context.Context, url, path string) error |
| 64 | IsRepository(ctx context.Context, path string) (bool, error) |
| 65 | |
| 66 | // Status and information |
| 67 | Status(ctx context.Context) (*Status, error) |
| 68 | Log(ctx context.Context, options LogOptions) ([]Commit, error) |
| 69 | Show(ctx context.Context, ref string) (*Commit, error) |
| 70 | |
| 71 | // Branch operations |
| 72 | ListBranches(ctx context.Context) ([]Branch, error) |
| 73 | CreateBranch(ctx context.Context, name string, startPoint string) error |
| 74 | Checkout(ctx context.Context, ref string) error |
| 75 | DeleteBranch(ctx context.Context, name string, force bool) error |
| 76 | GetCurrentBranch(ctx context.Context) (string, error) |
| 77 | |
| 78 | // Commit operations |
| 79 | Add(ctx context.Context, paths []string) error |
| 80 | AddAll(ctx context.Context) error |
| 81 | Commit(ctx context.Context, message string, options CommitOptions) error |
| 82 | |
| 83 | // Remote operations |
| 84 | ListRemotes(ctx context.Context) ([]Remote, error) |
| 85 | AddRemote(ctx context.Context, name, url string) error |
| 86 | RemoveRemote(ctx context.Context, name string) error |
| 87 | Fetch(ctx context.Context, remote string, options FetchOptions) error |
| 88 | Pull(ctx context.Context, remote, branch string) error |
| 89 | Push(ctx context.Context, remote, branch string, options PushOptions) error |
| 90 | |
| 91 | // Merge operations |
| 92 | Merge(ctx context.Context, ref string, options MergeOptions) error |
| 93 | MergeBase(ctx context.Context, ref1, ref2 string) (string, error) |
| 94 | |
| 95 | // Configuration |
| 96 | GetConfig(ctx context.Context, key string) (string, error) |
| 97 | SetConfig(ctx context.Context, key, value string) error |
| 98 | GetUserConfig(ctx context.Context) (*UserConfig, error) |
| 99 | SetUserConfig(ctx context.Context, config UserConfig) error |
| 100 | } |
| 101 | ``` |
| 102 | |
| 103 | ## Data Structures |
| 104 | |
| 105 | ### Status |
| 106 | |
| 107 | Represents the current state of the repository: |
| 108 | |
| 109 | ```go |
| 110 | type Status struct { |
| 111 | Branch string |
| 112 | IsClean bool |
| 113 | Staged []FileStatus |
| 114 | Unstaged []FileStatus |
| 115 | Untracked []string |
| 116 | Conflicts []string |
| 117 | } |
| 118 | ``` |
| 119 | |
| 120 | ### Commit |
| 121 | |
| 122 | Represents a Git commit: |
| 123 | |
| 124 | ```go |
| 125 | type Commit struct { |
| 126 | Hash string |
| 127 | Author Author |
| 128 | Committer Author |
| 129 | Message string |
| 130 | Parents []string |
| 131 | Timestamp time.Time |
| 132 | Files []CommitFile |
| 133 | } |
| 134 | ``` |
| 135 | |
| 136 | ### Branch |
| 137 | |
| 138 | Represents a Git branch: |
| 139 | |
| 140 | ```go |
| 141 | type Branch struct { |
| 142 | Name string |
| 143 | IsCurrent bool |
| 144 | IsRemote bool |
| 145 | Commit string |
| 146 | Message string |
| 147 | } |
| 148 | ``` |
| 149 | |
| 150 | ## Usage Examples |
| 151 | |
| 152 | ### Repository Management |
| 153 | |
| 154 | ```go |
| 155 | // Initialize a new repository |
| 156 | git := git.DefaultGit("/path/to/new/repo") |
| 157 | if err := git.Init(ctx, "/path/to/new/repo"); err != nil { |
| 158 | log.Fatal(err) |
| 159 | } |
| 160 | |
| 161 | // Clone a repository |
| 162 | if err := git.Clone(ctx, "https://github.com/user/repo.git", "/path/to/clone"); err != nil { |
| 163 | log.Fatal(err) |
| 164 | } |
| 165 | ``` |
| 166 | |
| 167 | ### Basic Git Workflow |
| 168 | |
| 169 | ```go |
| 170 | // Stage files |
| 171 | if err := git.Add(ctx, []string{"file1.txt", "file2.txt"}); err != nil { |
| 172 | log.Fatal(err) |
| 173 | } |
| 174 | |
| 175 | // Or stage all changes |
| 176 | if err := git.AddAll(ctx); err != nil { |
| 177 | log.Fatal(err) |
| 178 | } |
| 179 | |
| 180 | // Commit changes |
| 181 | commitOptions := git.CommitOptions{ |
| 182 | AllowEmpty: false, |
| 183 | } |
| 184 | if err := git.Commit(ctx, "Add new feature", commitOptions); err != nil { |
| 185 | log.Fatal(err) |
| 186 | } |
| 187 | ``` |
| 188 | |
| 189 | ### Branch Operations |
| 190 | |
| 191 | ```go |
| 192 | // List all branches |
| 193 | branches, err := git.ListBranches(ctx) |
| 194 | if err != nil { |
| 195 | log.Fatal(err) |
| 196 | } |
| 197 | |
| 198 | for _, branch := range branches { |
| 199 | fmt.Printf("Branch: %s (current: %t)\n", branch.Name, branch.IsCurrent) |
| 200 | } |
| 201 | |
| 202 | // Create a new branch |
| 203 | if err := git.CreateBranch(ctx, "feature/new-feature", ""); err != nil { |
| 204 | log.Fatal(err) |
| 205 | } |
| 206 | |
| 207 | // Switch to a branch |
| 208 | if err := git.Checkout(ctx, "feature/new-feature"); err != nil { |
| 209 | log.Fatal(err) |
| 210 | } |
| 211 | |
| 212 | // Get current branch |
| 213 | currentBranch, err := git.GetCurrentBranch(ctx) |
| 214 | if err != nil { |
| 215 | log.Fatal(err) |
| 216 | } |
| 217 | fmt.Printf("Current branch: %s\n", currentBranch) |
| 218 | ``` |
| 219 | |
| 220 | ### Remote Operations |
| 221 | |
| 222 | ```go |
| 223 | // Add a remote |
| 224 | if err := git.AddRemote(ctx, "origin", "https://github.com/user/repo.git"); err != nil { |
| 225 | log.Fatal(err) |
| 226 | } |
| 227 | |
| 228 | // List remotes |
| 229 | remotes, err := git.ListRemotes(ctx) |
| 230 | if err != nil { |
| 231 | log.Fatal(err) |
| 232 | } |
| 233 | |
| 234 | for _, remote := range remotes { |
| 235 | fmt.Printf("Remote: %s -> %s\n", remote.Name, remote.URL) |
| 236 | } |
| 237 | |
| 238 | // Fetch from remote |
| 239 | fetchOptions := git.FetchOptions{ |
| 240 | All: true, |
| 241 | Tags: true, |
| 242 | } |
| 243 | if err := git.Fetch(ctx, "", fetchOptions); err != nil { |
| 244 | log.Fatal(err) |
| 245 | } |
| 246 | |
| 247 | // Push to remote |
| 248 | pushOptions := git.PushOptions{ |
| 249 | SetUpstream: true, |
| 250 | } |
| 251 | if err := git.Push(ctx, "origin", "main", pushOptions); err != nil { |
| 252 | log.Fatal(err) |
| 253 | } |
| 254 | ``` |
| 255 | |
| 256 | ### Configuration |
| 257 | |
| 258 | ```go |
| 259 | // Get user configuration |
| 260 | userConfig, err := git.GetUserConfig(ctx) |
| 261 | if err != nil { |
| 262 | log.Fatal(err) |
| 263 | } |
| 264 | fmt.Printf("User: %s <%s>\n", userConfig.Name, userConfig.Email) |
| 265 | |
| 266 | // Set user configuration |
| 267 | newConfig := git.UserConfig{ |
| 268 | Name: "John Doe", |
| 269 | Email: "john@example.com", |
| 270 | } |
| 271 | if err := git.SetUserConfig(ctx, newConfig); err != nil { |
| 272 | log.Fatal(err) |
| 273 | } |
| 274 | |
| 275 | // Get/set custom config |
| 276 | value, err := git.GetConfig(ctx, "core.editor") |
| 277 | if err != nil { |
| 278 | log.Fatal(err) |
| 279 | } |
| 280 | |
| 281 | if err := git.SetConfig(ctx, "core.editor", "vim"); err != nil { |
| 282 | log.Fatal(err) |
| 283 | } |
| 284 | ``` |
| 285 | |
| 286 | ## Error Handling |
| 287 | |
| 288 | The Git interface provides detailed error information through the `GitError` type: |
| 289 | |
| 290 | ```go |
| 291 | if err := git.Commit(ctx, "Invalid commit", git.CommitOptions{}); err != nil { |
| 292 | if gitErr, ok := err.(*git.GitError); ok { |
| 293 | fmt.Printf("Git command '%s' failed: %v\n", gitErr.Command, gitErr.Err) |
| 294 | fmt.Printf("Output: %s\n", gitErr.Output) |
| 295 | } |
| 296 | } |
| 297 | ``` |
| 298 | |
| 299 | ## Configuration |
| 300 | |
| 301 | You can customize the Git instance with specific configuration: |
| 302 | |
| 303 | ```go |
| 304 | config := git.GitConfig{ |
| 305 | Timeout: 60 * time.Second, |
| 306 | Env: map[string]string{ |
| 307 | "GIT_AUTHOR_NAME": "Custom Author", |
| 308 | "GIT_AUTHOR_EMAIL": "author@example.com", |
| 309 | }, |
| 310 | } |
| 311 | |
| 312 | git := git.NewGit("/path/to/repo", config) |
| 313 | ``` |
| 314 | |
| 315 | ## Thread Safety |
| 316 | |
| 317 | The Git interface is not thread-safe. If you need to use it from multiple goroutines, you should either: |
| 318 | |
| 319 | 1. Use separate Git instances for each goroutine |
| 320 | 2. Use a mutex to synchronize access |
| 321 | 3. Use channels to serialize operations |
| 322 | |
| 323 | ## Requirements |
| 324 | |
| 325 | - Go 1.24.4 or later |
| 326 | - Git installed and available in PATH |
| 327 | - Appropriate permissions to access the repository |
| 328 | |
| 329 | ## License |
| 330 | |
| 331 | This code is part of the `github.com/iomodo/staff` project and follows the same license terms. |