package roast import ( "context" "errors" "fmt" "git-roast/internal/config" "git-roast/internal/log" "net/url" "os" "os/exec" "path/filepath" "strings" "time" "github.com/go-git/go-git/v5" ) type Roast struct { repo *git.Repository dir string } func getRepoOrigin() (string, error) { repo, err := git.PlainOpen(".") if err != nil { return "", errors.Join(errors.New("failed to open repository"), err) } origin, err := repo.Remote("origin") if err != nil { return "", errors.Join(errors.New("failed to open remote origin"), err) } originConfig := origin.Config() if originConfig == nil { return "", errors.New("failed to read origin config") } if len(originConfig.URLs) == 0 { return "", errors.New("no urls in remote origin") } return originConfig.URLs[0], nil } func Init() error { repoOriginUrl, err := getRepoOrigin() if err != nil { return err } id := url.PathEscape(repoOriginUrl) p := filepath.Join(config.ContentPath, id) initSuccessful := false defer func() { if !initSuccessful { _ = os.RemoveAll(p) } }() if err := os.RemoveAll(p); err != nil && !os.IsNotExist(err) { return err } var roastRepoOriginUrl string if s, hasSuffix := strings.CutSuffix(repoOriginUrl, ".git"); hasSuffix { roastRepoOriginUrl = s + ".roast.git" } else { roastRepoOriginUrl = s + ".roast" } { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() cmd := exec.CommandContext( ctx, "git", "clone", roastRepoOriginUrl, p, ) if out, err := cmd.CombinedOutput(); err != nil { log.Printf("failed to clone %s\n%s\n%s\n", roastRepoOriginUrl, out, err.Error()) } else { log.Debugf("cloned %s\n", roastRepoOriginUrl) repo, err := git.PlainOpen(p) if err != nil { return errors.Join(errors.New("failed to open roast repo"), err) } if commitIter, err := repo.CommitObjects(); err != nil { return errors.Join(errors.New("failed to get commits for roast repo"), err) } else if _, err := commitIter.Next(); err != nil { // repo has no commits // git commit --allow-empty -m init cmd = exec.Command("git", "commit", "--allow-empty", "-m", "init") cmd.Dir = p if out, err := cmd.CombinedOutput(); err != nil { return errors.Join(fmt.Errorf("failed to add initial commit to roast repo\n%s\n", out), err) } // git push -u origin main cmd = exec.Command("git", "push", "-u", "origin", "main") cmd.Dir = p if out, err := cmd.CombinedOutput(); err != nil { return errors.Join(fmt.Errorf("failed to push roast repo to %s\n%s\n", roastRepoOriginUrl, out), err) } log.Debugf("roast repository init pushed to %s\n", roastRepoOriginUrl) } initSuccessful = true return nil } } { // git init cmd := exec.Command("git", "init", p) if out, err := cmd.CombinedOutput(); err != nil { return errors.Join(fmt.Errorf("failed to init %s\n%s\n", roastRepoOriginUrl, out), err) } log.Debugf("init %s successful\n", roastRepoOriginUrl) // git remote add origin $roastRepoOriginUrl cmd = exec.Command("git", "remote", "add", "origin", roastRepoOriginUrl) cmd.Dir = p if out, err := cmd.CombinedOutput(); err != nil { return errors.Join(fmt.Errorf("failed to add remote %s to roast repo\n%s\n", roastRepoOriginUrl, out), err) } // git commit --allow-empty -m init cmd = exec.Command("git", "commit", "--allow-empty", "-m", "init") cmd.Dir = p if out, err := cmd.CombinedOutput(); err != nil { return errors.Join(fmt.Errorf("failed to add initial commit to roast repo\n%s\n", out), err) } // git push -u origin main cmd = exec.Command("git", "push", "-u", "origin", "main") cmd.Dir = p if out, err := cmd.CombinedOutput(); err != nil { return errors.Join(fmt.Errorf("failed to push roast repo to %s\n%s\n", roastRepoOriginUrl, out), err) } log.Debugf("new roast repository pushed to %s\n", roastRepoOriginUrl) } initSuccessful = true return nil } func Open() (*Roast, error) { repoOriginUrl, err := getRepoOrigin() if err != nil { return nil, err } id := url.PathEscape(repoOriginUrl) p := filepath.Join(config.ContentPath, id) if stat, err := os.Stat(p); err != nil { if os.IsNotExist(err) { return nil, errors.New("roast repo not initialized, use `git roast init`") } else { return nil, errors.Join(errors.New("failed to check if roast dir for repo exists"), err) } } else if !stat.IsDir() { return nil, errors.Join(errors.New("path for roast dir for repo exists but is not a directory"), err) } roastRepo, err := git.PlainOpen(p) if err != nil { return nil, errors.Join(fmt.Errorf("failed to open roast repo %s", p), err) } pull := exec.Command("git", "pull", "--force") pull.Dir = p if err := pull.Run(); err != nil { return nil, errors.Join(fmt.Errorf("failed to pull roast repo %s", p), err) } return &Roast{ repo: roastRepo, dir: p, }, nil }