package main import ( "context" "flag" "fmt" "os" "os/exec" "os/signal" "path" "path/filepath" "strings" "github.com/npfaro/dumbforge/internal/gitutil" "github.com/npfaro/dumbforge/internal/helper" "github.com/npfaro/dumbforge/internal/publish" "github.com/npfaro/dumbforge/internal/remoteurl" "github.com/npfaro/dumbforge/internal/s3store" "github.com/npfaro/dumbforge/internal/site" ) var version = "0.1.0-dev" func main() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() base := strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe") if base == "git-remote-dumbforge" { if len(os.Args) < 3 { die("git remote helper requires a remote name and URL") } if err := helper.Run(ctx, os.Args[2], os.Stdin, os.Stdout, os.Stderr); err != nil { die("%v", err) } return } if len(os.Args) < 2 { usage() os.Exit(2) } switch os.Args[1] { case "version", "--version", "-v": fmt.Printf("dumbforge %s\n", version) case "remote": if err := remoteCommand(os.Args[2:]); err != nil { die("%v", err) } case "site": if err := siteCommand(ctx, os.Args[2:]); err != nil { die("%v", err) } case "__remote-helper": if len(os.Args) < 4 { die("internal remote helper requires a remote name and URL") } if err := helper.Run(ctx, os.Args[3], os.Stdin, os.Stdout, os.Stderr); err != nil { die("%v", err) } default: usage() os.Exit(2) } } func siteCommand(ctx context.Context, args []string) (err error) { if len(args) == 0 || args[0] != "publish" { return fmt.Errorf("usage: dumbforge site publish [--remote ] [--branch ]") } flags := flag.NewFlagSet("site publish", flag.ContinueOnError) remoteName := flags.String("remote", "origin", "configured Git remote") branch := flags.String("branch", "", "branch name; defaults to the current branch") if err := flags.Parse(args[1:]); err != nil { return err } if *branch == "" { out, err := gitOutput("branch", "--show-current") if err != nil { return err } *branch = strings.TrimSpace(string(out)) } if *branch == "" { return fmt.Errorf("could not determine a branch; pass --branch") } pushURLBytes, err := gitOutput("config", "--get", "remote."+*remoteName+".pushurl") if err != nil { return fmt.Errorf("remote %s has no dumbforge push URL", *remoteName) } spec, err := remoteurl.Parse(strings.TrimSpace(string(pushURLBytes))) if err != nil { return err } store, err := s3store.New(ctx, spec) if err != nil { return err } lock, err := store.AcquireLock(ctx) if err != nil { return err } defer func() { if releaseErr := lock.Release(context.WithoutCancel(ctx)); err == nil && releaseErr != nil { err = releaseErr } }() git := gitutil.Git{} oid, err := git.RevParse(ctx, "refs/heads/"+*branch) if err != nil { oid, err = git.RevParse(ctx, "HEAD") if err != nil { return err } } metadata, err := publish.LoadMetadata(ctx, store) if err != nil { return err } owner := spec.Bucket prefixDir := path.Dir(spec.Prefix) if prefixDir != "." && prefixDir != "/" { owner = path.Base(prefixDir) } generator, err := site.New(git, spec.PublicRepoURL(), path.Base(spec.Prefix), owner) if err != nil { return err } generator.Progress = func(format string, args ...any) { fmt.Printf("dumbforge: "+format+"\n", args...) } generator.SetBranches(metadata.BranchNames(), strings.TrimPrefix(metadata.Head, "refs/heads/")) generator.SetAssetsCurrent(metadata.SiteVersion == generator.AssetVersion()) build, err := generator.BuildBranch(ctx, *branch, "", oid, metadata.Head == "refs/heads/"+*branch) if err != nil { return err } navigation, err := generator.RepositoryNavigationBuild(nil) if err != nil { return err } build.Pages = append(build.Pages, navigation.Pages...) fmt.Printf("dumbforge: generated %d site object(s)\n", len(build.Pages)) if err := lock.Refresh(ctx); err != nil { return err } return generator.Publish(ctx, store, build) } func remoteCommand(args []string) error { if len(args) < 2 || args[0] != "add" { return fmt.Errorf("usage: dumbforge remote add [options]") } name := args[1] flags := flag.NewFlagSet("remote add", flag.ContinueOnError) bucket := flags.String("bucket", "", "S3 bucket") prefix := flags.String("prefix", "", "repository object prefix") endpoint := flags.String("endpoint", "", "S3-compatible API endpoint") profile := flags.String("profile", "", "AWS shared-configuration profile") publicURL := flags.String("public-url", "", "public bucket base URL") if err := flags.Parse(args[2:]); err != nil { return err } if *prefix == "" { *prefix = name + ".git" } spec := remoteurl.Spec{ Bucket: *bucket, Prefix: *prefix, Endpoint: *endpoint, Profile: *profile, PublicURL: *publicURL, } pushURL, err := remoteurl.Build(spec) if err != nil { return err } fetchURL := spec.PublicRepoURL() if fetchURL == "" { return fmt.Errorf("public URL is required") } remoteArgs := []string{"remote", "add"} if isMirrorRepository() { remoteArgs = append(remoteArgs, "--mirror=push") } remoteArgs = append(remoteArgs, "--", name, fetchURL) if err := runGit(remoteArgs...); err != nil { return err } if err := runGit("config", "--add", "remote."+name+".pushurl", pushURL); err != nil { return err } fmt.Printf("added %s\n fetch: %s\n push: dumbforge://%s/%s\n", name, fetchURL, spec.Bucket, spec.Prefix) return nil } func isMirrorRepository() bool { out, err := gitOutput("config", "--type=bool", "--get-regexp", `^remote\..*\.mirror$`) if err != nil { return false } for _, line := range strings.Split(string(out), "\n") { fields := strings.Fields(line) if len(fields) == 2 && fields[1] == "true" { return true } } return false } func runGit(args ...string) error { // #nosec G204 G702 -- arguments are passed directly to Git without a shell. cmd := exec.Command("git", args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("git %s failed: %w", strings.Join(args, " "), err) } return nil } func gitOutput(args ...string) ([]byte, error) { // #nosec G204 -- arguments are passed directly to Git without a shell. cmd := exec.Command("git", args...) var stderr strings.Builder cmd.Stderr = &stderr out, err := cmd.Output() if err != nil { return nil, fmt.Errorf("git %s: %s", strings.Join(args, " "), strings.TrimSpace(stderr.String())) } return out, nil } func usage() { fmt.Fprintln(os.Stderr, `dumbforge stores Git repositories in S3-compatible object storage. Usage: dumbforge remote add --bucket --prefix \ --endpoint --profile --public-url dumbforge site publish --remote [--branch ] dumbforge version`) } func die(format string, args ...any) { fmt.Fprintf(os.Stderr, "dumbforge: "+format+"\n", args...) os.Exit(1) }