gflakestat/main.go

190 lines
4.1 KiB
Go
Raw Normal View History

2024-07-08 00:40:48 +00:00
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)
}
}