190 lines
4.1 KiB
Go
190 lines
4.1 KiB
Go
|
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)
|
||
|
}
|
||
|
}
|