package main import ( "context" "encoding/json" "fmt" "os" "os/exec" "time" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/storer" "github.com/go-git/go-git/v5/storage/memory" ) type UnixTimestamp struct { time.Time } func (ut *UnixTimestamp) UnmarshalJSON(bytes []byte) error { var raw int64 err := json.Unmarshal(bytes, &raw) if err != nil { fmt.Printf("error decoding timestamp: %s\n", err) return err } ut.Time = time.Unix(raw, 0) return nil } type Config struct { FlakePath string Interval time.Duration } type FlakeMetadata struct { Description string `json:"description"` LastModified UnixTimestamp `json:"lastModified"` Locks struct { Nodes map[string]struct { Locked struct { LastModified UnixTimestamp `json:"lastModified"` NARHash string `json:"narHash"` Owner string `json:"owner"` Repo string `json:"repo"` Rev string `json:"rev"` Ref string `json:"ref"` Type string `json:"type"` URL string `json:"url"` } `json:"locked"` Original struct { Owner string `json:"owner"` Repo string `json:"repo"` Type string `json:"type"` Ref string `json:"ref"` } `json:"original"` } `json:"nodes"` } `json:"locks"` } func LoadConfig() (*Config, error) { var c Config if path := os.Getenv("GFLAKESTAT_PATH"); path != "" { c.FlakePath = path } else { return nil, fmt.Errorf("GFLAKESTAT_PATH not set or empty") } if interval := os.Getenv("GFLAKESTAT_INTERVAL"); interval != "" { if dur, err := time.ParseDuration(interval); err != nil { return nil, fmt.Errorf("failed parsing duration: %w", err) } else { c.Interval = dur } } else { c.Interval = 30 * time.Minute } return &c, nil } func ReadFlakeMetadata(cfg *Config) (*FlakeMetadata, error) { args := []string{ "flake", "metadata", "--json", cfg.FlakePath, } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "nix", args...) output, err := cmd.Output() if err != nil { return nil, err } var metadata FlakeMetadata if err := json.Unmarshal(output, &metadata); err != nil { return nil, err } return &metadata, nil } func main() { cfg, err := LoadConfig() if err != nil { fmt.Printf("Error loading config from env: %s\n", err) os.Exit(1) } metadata, err := ReadFlakeMetadata(cfg) if err != nil { fmt.Printf("Error reading flake metadata: %s\n", err) os.Exit(1) } for key, val := range metadata.Locks.Nodes { if key != "nixpkgs" { continue } var url string switch val.Locked.Type { case "github": url = fmt.Sprintf("https://github.com/%s/%s.git", val.Locked.Owner, val.Locked.Repo) case "git": url = val.Locked.URL } branchName := "refs/heads/nixos-unstable" repo, err := git.Init(memory.NewStorage(), nil) if err != nil { panic(err) } if _, err := repo.CreateRemote(&config.RemoteConfig{ Name: "origin", URLs: []string{url}, }); err != nil { panic(err) } fmt.Println("Starting fetch") err = repo.Fetch(&git.FetchOptions{ RemoteName: "origin", RefSpecs: []config.RefSpec{config.RefSpec(branchName + ":" + branchName)}, Depth: 1, // Shallow fetch to minimize data transfer }) if err != nil { panic(err) } fmt.Println("Fetch done") ref, err := repo.Reference(plumbing.ReferenceName(branchName), true) if err != nil { panic(err) } latestCommit, err := repo.CommitObject(ref.Hash()) if err != nil { panic(err) } startCommit, err := repo.CommitObject(plumbing.NewHash(val.Locked.Ref)) if err != nil { panic(err) } count := 0 commitIter := latestCommit.Parents() err = commitIter.ForEach(func(commit *object.Commit) error { count++ if commit.Hash == startCommit.Hash { return storer.ErrStop } return nil }) if err != nil { panic(err) } fmt.Printf("%s: %s: %d\n", key, url, count) } }