package helper import ( "bufio" "context" "fmt" "io" "path" "strings" "time" "github.com/npfaro/dumbforge/internal/gitutil" "github.com/npfaro/dumbforge/internal/publish" "github.com/npfaro/dumbforge/internal/remoteurl" "github.com/npfaro/dumbforge/internal/s3store" "github.com/npfaro/dumbforge/internal/site" ) func Run(ctx context.Context, rawURL string, stdin io.Reader, stdout, stderr io.Writer) error { spec, err := remoteurl.Parse(rawURL) if err != nil { return err } store, err := s3store.New(ctx, spec) if err != nil { return err } publisher := &publish.Publisher{ Store: store, Git: gitutil.Git{}, Progress: func(format string, args ...any) { fmt.Fprintf(stderr, "dumbforge: "+format+"\n", args...) }, } publisher.AfterPublish = func(ctx context.Context, result publish.Result) { publishSite(ctx, publisher, result, stderr) } defer func() { if err := publisher.Abort(context.WithoutCancel(ctx)); err != nil { fmt.Fprintf(stderr, "dumbforge: warning: release unused push lock: %v\n", err) } }() scanner := bufio.NewScanner(stdin) writer := bufio.NewWriter(stdout) var advertised map[string]string for scanner.Scan() { line := scanner.Text() switch { case line == "capabilities": fmt.Fprintln(writer, "push") fmt.Fprintln(writer, "option") fmt.Fprintln(writer) if err := writer.Flush(); err != nil { return err } case line == "list" || line == "list for-push": var metadata publish.Metadata var err error if line == "list for-push" { metadata, err = publisher.Prepare(ctx) } else { metadata, err = publisher.List(ctx) } if err != nil { return err } advertised = make(map[string]string, len(metadata.Refs)) for name, ref := range metadata.Refs { advertised[name] = ref.OID } writeRefList(writer, metadata, line == "list") if err := writer.Flush(); err != nil { return err } case strings.HasPrefix(line, "option "): name := strings.Fields(line) if len(name) >= 2 && (name[1] == "verbosity" || name[1] == "progress") { fmt.Fprintln(writer, "ok") } else { fmt.Fprintln(writer, "unsupported") } if err := writer.Flush(); err != nil { return err } case strings.HasPrefix(line, "push "): lines := []string{strings.TrimPrefix(line, "push ")} for scanner.Scan() { next := scanner.Text() if next == "" { break } if !strings.HasPrefix(next, "push ") { return fmt.Errorf("unexpected command in push batch %q", next) } lines = append(lines, strings.TrimPrefix(next, "push ")) } updates, err := parsePushes(lines) if err == nil { _, err = publisher.Push(ctx, updates, advertised) } if err != nil { message := strings.NewReplacer("\n", " ", "\r", " ").Replace(err.Error()) for _, update := range updates { fmt.Fprintf(writer, "error %s %s\n", update.Dst, message) } if len(updates) == 0 { return err } } else { for _, update := range updates { fmt.Fprintf(writer, "ok %s\n", update.Dst) } } fmt.Fprintln(writer) if err := writer.Flush(); err != nil { return err } case line == "": // Empty lines terminate command batches and need no response here. default: return fmt.Errorf("unsupported remote-helper command %q", line) } } return scanner.Err() } func publishSite(ctx context.Context, publisher *publish.Publisher, result publish.Result, stderr io.Writer) { spec := publisher.Store.Spec() if spec.PublicRepoURL() == "" { return } owner := spec.Bucket prefixDir := path.Dir(spec.Prefix) if prefixDir != "." && prefixDir != "/" { owner = path.Base(prefixDir) } generator, err := site.New(publisher.Git, spec.PublicRepoURL(), path.Base(spec.Prefix), owner) if err != nil { fmt.Fprintf(stderr, "dumbforge: warning: generate site: %v\n", err) return } generator.Progress = func(format string, args ...any) { fmt.Fprintf(stderr, "dumbforge: "+format+"\n", args...) } generator.SetBranches(result.Branches, strings.TrimPrefix(result.Head, "refs/heads/")) generator.SetAssetsCurrent(result.SiteVersion == generator.AssetVersion()) var deletedBranches []string navigationChanged := false for _, update := range result.Updates { if !strings.HasPrefix(update.Dst, "refs/heads/") { continue } branch := strings.TrimPrefix(update.Dst, "refs/heads/") if update.NewOID == "" { deletedBranches = append(deletedBranches, branch) navigationChanged = true continue } if update.OldOID == "" { navigationChanged = true } generatedAt := time.Now() build, err := generator.BuildBranch(ctx, branch, update.OldOID, update.NewOID, update.Dst == result.Head) generationTime := time.Since(generatedAt) if err == nil { publishedAt := time.Now() err = generator.Publish(ctx, publisher.Store, build) if err == nil { generator.SetAssetsCurrent(true) fmt.Fprintf(stderr, "dumbforge: site generated in %s, uploaded in %s\n", generationTime.Round(time.Millisecond), time.Since(publishedAt).Round(time.Millisecond)) } } if err != nil { fmt.Fprintf(stderr, "dumbforge: warning: site publication failed: %v\n", err) } } if navigationChanged { navigation, err := generator.RepositoryNavigationBuild(deletedBranches) if err == nil { err = generator.Publish(ctx, publisher.Store, navigation) } if err != nil { fmt.Fprintf(stderr, "dumbforge: warning: repository navigation publication failed: %v\n", err) } } } func parsePushes(specs []string) ([]publish.Update, error) { updates := make([]publish.Update, 0, len(specs)) for _, spec := range specs { force := strings.HasPrefix(spec, "+") spec = strings.TrimPrefix(spec, "+") parts := strings.SplitN(spec, ":", 2) if len(parts) != 2 || parts[1] == "" { return nil, fmt.Errorf("invalid push refspec %q", spec) } updates = append(updates, publish.Update{Src: parts[0], Dst: parts[1], Force: force}) } return updates, nil } func sortedRefNames(refs map[string]publish.Ref) []string { result := make([]string, 0, len(refs)) for name := range refs { result = append(result, name) } for i := 1; i < len(result); i++ { for j := i; j > 0 && result[j] < result[j-1]; j-- { result[j], result[j-1] = result[j-1], result[j] } } return result } func writeRefList(writer io.Writer, metadata publish.Metadata, includeHead bool) { if includeHead && metadata.Head != "" { fmt.Fprintf(writer, "@%s HEAD\n", metadata.Head) } for _, name := range sortedRefNames(metadata.Refs) { fmt.Fprintf(writer, "%s %s\n", metadata.Refs[name].OID, name) } fmt.Fprintln(writer) }