package xgit import ( "context" "os" "path/filepath" "strings" "time" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/ignite/cli/v29/ignite/pkg/errors" ) var ( commitMsg = "Initialized with Ignite CLI" defaultOpenOpts = git.PlainOpenOptions{DetectDotGit: true} devXAuthor = &object.Signature{ Name: "Developer Experience team at Ignite", Email: "hello@ignite.com", When: time.Now(), } ) // InitAndCommit creates a git repo in path if path isn't already inside a git // repository, then commits path content. func InitAndCommit(path string) error { repo, err := git.PlainOpenWithOptions(path, &defaultOpenOpts) if err != nil { if !errors.Is(err, git.ErrRepositoryNotExists) { return errors.Errorf("open git repo %s: %w", path, err) } // not a git repo, creates a new one repo, err = git.PlainInitWithOptions(path, &git.PlainInitOptions{ InitOptions: git.InitOptions{ DefaultBranch: plumbing.Main, }, Bare: false, }) if err != nil { return errors.Errorf("init git repo %s: %w", path, err) } } wt, err := repo.Worktree() if err != nil { return errors.Errorf("worktree %s: %w", path, err) } // wt.Add(path) takes only relative path, we need to turn path relative to // repo path. repoPath := wt.Filesystem.Root() path, err = filepath.Rel(repoPath, path) if err != nil { return errors.Errorf("find relative path %s %s: %w", repoPath, path, err) } if _, err := wt.Add(path); err != nil { return errors.Errorf("git add %s: %w", path, err) } _, err = wt.Commit(commitMsg, &git.CommitOptions{ All: true, Author: devXAuthor, }) if err != nil { return errors.Errorf("git commit %s: %w", path, err) } return nil } // AreChangesCommitted returns true if dir is a clean git repository with no // pending changes. It returns also true if dir is NOT a git repository. func AreChangesCommitted(dir string) (bool, error) { dir, err := filepath.Abs(dir) if err != nil { return false, err } repository, err := git.PlainOpen(dir) if err != nil { if errors.Is(err, git.ErrRepositoryNotExists) { return true, nil } return false, err } w, err := repository.Worktree() if err != nil { return false, err } ws, err := w.Status() if err != nil { return false, err } return ws.IsClean(), nil } // Clone clones a git repository represented by urlRef, into dir. // urlRef is the URL of the repository, with an optional ref, suffixed to the // URL with a `@`. Ref can be a tag, a branch or a hash. // Valid examples of urlRef: github.com/org/repo, github.com/org/repo@v1, // github.com/org/repo@develop, github.com/org/repo@ab88cdf. func Clone(ctx context.Context, urlRef, dir string) error { // Ensure dir is empty if it exists (if it doesn't exist, the call to // git.PlainCloneContext below will create it). files, _ := os.ReadDir(dir) if len(files) > 0 { return errors.Errorf("clone: target directory %q is not empty", dir) } // Split urlRef var ( parts = strings.Split(urlRef, "@") url = parts[0] ref string ) if len(parts) > 1 { ref = parts[1] } // First clone the repo repo, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{ URL: url, }) if err != nil { return err } if ref == "" { // if ref is not provided, job is done return nil } // Reference provided, try to resolve wt, err := repo.Worktree() if err != nil { return err } var h *plumbing.Hash for _, ref := range []string{ref, "origin/" + ref} { h, err = repo.ResolveRevision(plumbing.Revision(ref)) if err == nil { break } } if err != nil { // Ref not found, clean up dir and return error os.RemoveAll(dir) return err } return wt.Checkout(&git.CheckoutOptions{ Hash: *h, }) } // IsRepository checks if a path contains a Git repository. func IsRepository(path string) (bool, error) { if _, err := git.PlainOpenWithOptions(path, &defaultOpenOpts); err != nil { if errors.Is(err, git.ErrRepositoryNotExists) { return false, nil } return false, err } return true, nil } // RepositoryURL returns the URL of the origin remote of a Git repository. func RepositoryURL(path string) (string, error) { repo, err := git.PlainOpenWithOptions(path, &defaultOpenOpts) if err != nil { return "", err } cfg, err := repo.Config() if err != nil { return "", err } origin, ok := cfg.Remotes["origin"] if !ok { return "", errors.Errorf("no origin remote found in %s", path) } return origin.URLs[0], nil }