140 lines · 3.6 KB · Go
RawDownload
package cli import (	"context"	"encoding/json"	"flag"	"fmt"	"io"	"os"	"path/filepath"	"strings"	"time" 	"github.com/openclaw/gitcrawl/internal/capture") // runCapture exports one deterministic code-free repository snapshot.func (a *App) runCapture(ctx context.Context, args []string) error {	fs := flag.NewFlagSet("capture", flag.ContinueOnError)	fs.SetOutput(io.Discard)	schema := fs.String("schema", capture.SchemaV1, "capture schema")	output := fs.String("output", "", "write capture atomically to this path")	since := fs.String("since", "", "include threads updated at or after this RFC3339 instant")	jsonOut := fs.Bool("json", false, "write JSON output")	if err := fs.Parse(normalizeCommandArgs(args, map[string]bool{		"schema": true,		"output": true,		"since":  true,	})); err != nil {		return usageErr(err)	}	a.applyCommandJSON(*jsonOut)	if fs.NArg() != 1 {		return usageErr(fmt.Errorf("capture requires owner/repo"))	}	if strings.TrimSpace(*schema) != capture.SchemaV1 {		return usageErr(fmt.Errorf(			"unsupported capture schema %q",			strings.TrimSpace(*schema),		))	}	owner, repoName, err := parseOwnerRepo(fs.Arg(0))	if err != nil {		return usageErr(err)	}	rt, err := a.openLocalRuntimeReadOnly(ctx)	if err != nil {		return err	}	defer rt.Store.Close()	rateLimit, err := a.captureRateLimit(ctx)	if err != nil {		return err	}	result, err := capture.Build(		ctx,		rt.Store,		owner+"/"+repoName,		version,		rateLimit,		*since,	)	if err != nil {		return err	}	data, err := json.MarshalIndent(result, "", "  ")	if err != nil {		return fmt.Errorf("encode capture: %w", err)	}	data = append(data, '\n')	if strings.TrimSpace(*output) == "" {		_, err = a.Stdout.Write(data)		return err	}	return writeCaptureAtomically(*output, data)} func (a *App) captureRateLimit(	ctx context.Context,) (capture.RateLimit, error) {	state, ok := a.sharedRateLimitState(ctx)	if !ok {		return capture.RateLimit{}, fmt.Errorf(			"capture rate limit observation is unavailable; run sync with the same GitHub token first",		)	}	var resetAt *string	if !state.ResetAt.IsZero() {		formatted := state.ResetAt.UTC().Format(time.RFC3339Nano)		resetAt = &formatted	}	return capture.RateLimit{		Resource:   state.Resource,		Limit:      state.Limit,		Remaining:  state.Remaining,		ResetAt:    resetAt,		ObservedAt: state.UpdatedAt.UTC().Format(time.RFC3339Nano),	}, nil} // writeCaptureAtomically publishes a private file without a partial target.func writeCaptureAtomically(output string, data []byte) error {	output = filepath.Clean(strings.TrimSpace(output))	if output == "." || output == "" {		return fmt.Errorf("capture output path is empty")	}	parent := filepath.Dir(output)	if err := os.MkdirAll(parent, 0o700); err != nil {		return fmt.Errorf("create capture output directory: %w", err)	}	temp, err := os.CreateTemp(parent, "."+filepath.Base(output)+".tmp-*")	if err != nil {		return fmt.Errorf("create capture output: %w", err)	}	tempPath := temp.Name()	published := false	defer func() {		_ = temp.Close()		if !published {			_ = os.Remove(tempPath)		}	}()	if err := temp.Chmod(0o600); err != nil {		return fmt.Errorf("protect capture output: %w", err)	}	if _, err := temp.Write(data); err != nil {		return fmt.Errorf("write capture output: %w", err)	}	if err := temp.Sync(); err != nil {		return fmt.Errorf("sync capture output: %w", err)	}	if err := temp.Close(); err != nil {		return fmt.Errorf("close capture output: %w", err)	}	if err := os.Rename(tempPath, output); err != nil {		return fmt.Errorf("publish capture output: %w", err)	}	published = true	return nil}