aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.envrc1
-rw-r--r--.gitignore2
-rw-r--r--README.org1
-rw-r--r--cmd/git-leaderboard/main.go128
-rw-r--r--cmd/goget/main.go165
-rw-r--r--cmd/pviz/main.go266
-rw-r--r--cmd/seq-stat/main.go115
-rw-r--r--flake.lock142
-rw-r--r--flake.nix135
-rw-r--r--go.mod20
-rw-r--r--go.sum38
-rw-r--r--nix/modules/goget.nix66
-rw-r--r--nix/overlay.nix5
-rw-r--r--nix/packages/default.nix5
-rw-r--r--nix/packages/goget.nix31
-rw-r--r--treefmt.nix30
16 files changed, 1150 insertions, 0 deletions
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..3550a30
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6d69087
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.pre-commit-config.yaml
+/result
diff --git a/README.org b/README.org
new file mode 100644
index 0000000..19764b6
--- /dev/null
+++ b/README.org
@@ -0,0 +1 @@
+My experimental monorepo for Go tools and packages.
diff --git a/cmd/git-leaderboard/main.go b/cmd/git-leaderboard/main.go
new file mode 100644
index 0000000..7c846a5
--- /dev/null
+++ b/cmd/git-leaderboard/main.go
@@ -0,0 +1,128 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "os/exec"
+ "sort"
+ "strings"
+)
+
+type Author struct {
+ Name string
+ Count int
+}
+
+func main() {
+ lineFlag := flag.Bool("line", false, "Count lines changed instead of commits")
+ flag.Parse()
+
+ if !isGitRepository() {
+ fmt.Println("Error: Not in a git repository")
+ os.Exit(1)
+ }
+
+ authors, err := getAuthors(*lineFlag)
+ if err != nil {
+ fmt.Printf("Error getting authors: %v\n", err)
+ os.Exit(1)
+ }
+
+ if len(authors) == 0 {
+ fmt.Println("No authors found. The repository might be empty or have no commits.")
+ os.Exit(0)
+ }
+
+ sort.Slice(authors, func(i, j int) bool {
+ return authors[i].Count > authors[j].Count
+ })
+
+ maxCount := authors[0].Count
+
+ fmt.Printf("%-30s %-10s %s\n", "Author", "Count", "Contribution")
+ fmt.Println(strings.Repeat("-", 80))
+
+ for _, author := range authors {
+ bar := generateBar(author.Count, maxCount)
+ fmt.Printf("%-30s %-10d %s\n", author.Name, author.Count, bar)
+ }
+}
+
+func isGitRepository() bool {
+ cmd := exec.Command("git", "rev-parse", "--is-inside-work-tree")
+ err := cmd.Run()
+ return err == nil
+}
+
+func getAuthors(countLines bool) ([]Author, error) {
+ var cmd *exec.Cmd
+
+ if countLines {
+ cmd = exec.Command("git", "log", "--pretty=format:%aN", "--numstat")
+ } else {
+ cmd = exec.Command("git", "shortlog", "-sn", "--no-merges", "HEAD")
+ }
+
+ output, err := cmd.Output()
+ if err != nil {
+ if exitErr, ok := err.(*exec.ExitError); ok {
+ return nil, fmt.Errorf("git command failed: %s. Error output: %s", err, exitErr.Stderr)
+ }
+ return nil, fmt.Errorf("error executing git command: %v", err)
+ }
+
+ lines := strings.Split(string(output), "\n")
+ authors := make(map[string]int)
+
+ if countLines {
+ currentAuthor := ""
+ for _, line := range lines {
+ if line == "" {
+ currentAuthor = ""
+ continue
+ }
+ if currentAuthor == "" {
+ currentAuthor = line
+ continue
+ } else {
+ fields := strings.Fields(line)
+ if len(fields) >= 2 {
+ added, _ := parseInt(fields[0])
+ deleted, _ := parseInt(fields[1])
+ authors[currentAuthor] += added + deleted
+ }
+ }
+ }
+ } else {
+ for _, line := range lines {
+ fields := strings.Fields(line)
+ if len(fields) >= 2 {
+ count, _ := parseInt(fields[0])
+ name := strings.Join(fields[1:], " ")
+ authors[name] = count
+ }
+ }
+ }
+
+ var result []Author
+ for name, count := range authors {
+ result = append(result, Author{Name: name, Count: count})
+ }
+ return result, nil
+}
+
+func parseInt(s string) (int, error) {
+ if s == "-" {
+ return 0, nil
+ }
+ var result int
+ _, err := fmt.Sscanf(s, "%d", &result)
+ return result, err
+}
+
+func generateBar(count, maxCount int) string {
+ maxWidth := 38
+ width := int(float64(count) / float64(maxCount) * float64(maxWidth))
+ return strings.Repeat("▆", width)
+}
diff --git a/cmd/goget/main.go b/cmd/goget/main.go
new file mode 100644
index 0000000..3f17448
--- /dev/null
+++ b/cmd/goget/main.go
@@ -0,0 +1,165 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "os/signal"
+ "slices"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promauto"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+ "golang.org/x/mod/semver"
+)
+
+const (
+ forgeDomain = "code.fcuny.net"
+ forgeUser = "fcuny"
+ goPkgDomain = "go.fcuny.net"
+)
+
+var (
+ goGetReqs = promauto.NewCounterVec(prometheus.CounterOpts{
+ Name: "goget_requests_total",
+ Help: "go get requests processed, by repository name.",
+ }, []string{"name"})
+
+ modules = []string{"x"}
+)
+
+func main() {
+ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
+ defer cancel()
+
+ metricsMux := http.NewServeMux()
+ metricsMux.Handle("/metrics", promhttp.Handler())
+ metricsServer := &http.Server{
+ Addr: ":9091", Handler: metricsMux,
+ ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second,
+ }
+
+ go func() {
+ log.Println("Starting metrics server on :9091")
+ if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ log.Printf("Metrics server error: %v", err)
+ }
+ }()
+
+ s := &http.Server{
+ Addr: ":8070",
+ Handler: handler(),
+ ReadTimeout: 10 * time.Second,
+ WriteTimeout: 10 * time.Second,
+ IdleTimeout: 1 * time.Minute,
+ }
+
+ go func() {
+ log.Println("Starting main server on :8080")
+ if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ log.Printf("Main server error: %v", err)
+ }
+ }()
+
+ <-ctx.Done()
+ log.Println("Shutdown signal received, starting graceful shutdown...")
+
+ shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer shutdownCancel()
+
+ var shutdownErr error
+
+ log.Println("Shutting down main server...")
+ if err := s.Shutdown(shutdownCtx); err != nil {
+ log.Printf("Main server shutdown error: %v", err)
+ shutdownErr = err
+ }
+
+ log.Println("Shutting down metrics server...")
+ if err := metricsServer.Shutdown(shutdownCtx); err != nil {
+ log.Printf("Metrics server shutdown error: %v", err)
+ if shutdownErr == nil {
+ shutdownErr = err
+ }
+ }
+
+ if shutdownErr != nil {
+ log.Printf("Shutdown completed with errors")
+ os.Exit(1)
+ }
+
+ log.Println("Shutdown completed successfully")
+}
+
+func handler() http.Handler {
+ mux := http.NewServeMux()
+
+ mux.Handle(goPkgDomain+"/{name}", siteHandler(modules))
+ mux.Handle(goPkgDomain+"/{name}/", siteHandler(modules))
+
+ goGetMux := http.NewServeMux()
+ for _, name := range modules {
+ module := goPkgDomain + "/" + name
+ goGetMux.Handle(
+ module+"/",
+ goImportHandler(module, "https://"+forgeDomain+"/"+forgeUser+"/"+name),
+ )
+ }
+
+ return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ status := http.StatusMethodNotAllowed
+ http.Error(rw, http.StatusText(status), status)
+ return
+ }
+
+ if r.URL.Query().Get("go-get") == "1" {
+ goGetMux.ServeHTTP(rw, r)
+ return
+ }
+ mux.ServeHTTP(rw, r)
+ })
+}
+
+func goImportHandler(module, repo string) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ goGetReqs.WithLabelValues(module).Inc()
+ w.Header().Set("Content-Type", "text/html; charset=UTF-8")
+ if _, err := fmt.Fprintf(w, `<head><meta name="go-import" content="%s git %s">`, module, repo); err != nil {
+ log.Printf("Error writing response: %v", err)
+ }
+ })
+}
+
+func siteHandler(names []string) http.Handler {
+ return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+ name := r.PathValue("name")
+ name, version, hasV := strings.Cut(name, "@")
+ if !hasV {
+ name, _, _ = strings.Cut(name, ".")
+ }
+ if hasV && !semver.IsValid(version) || !slices.Contains(names, name) {
+ http.NotFound(rw, r)
+ return
+ }
+ u := &url.URL{
+ Scheme: "https",
+ Host: forgeDomain,
+ Path: forgeUser + r.URL.Path,
+ }
+ if !hasV {
+ path, symbol, hasSymbol := strings.Cut(r.URL.Path, ".")
+ if hasSymbol {
+ u.Path = forgeUser + "/" + path
+ u.Fragment = symbol
+ }
+ }
+ http.Redirect(rw, r, u.String(), http.StatusFound)
+ })
+}
diff --git a/cmd/pviz/main.go b/cmd/pviz/main.go
new file mode 100644
index 0000000..89bd78d
--- /dev/null
+++ b/cmd/pviz/main.go
@@ -0,0 +1,266 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+ "text/tabwriter"
+
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
+)
+
+type Period string
+
+const (
+ PeriodDay Period = "day"
+ PeriodWeek Period = "week"
+ PeriodMonth Period = "month"
+ PeriodQuarter Period = "quarter"
+ PeriodYear Period = "year"
+)
+
+var periodMinutes = map[Period]float64{
+ PeriodDay: 24 * 60,
+ PeriodWeek: 7 * 24 * 60,
+ PeriodMonth: 30 * 24 * 60,
+ PeriodQuarter: 90 * 24 * 60,
+ PeriodYear: 365 * 24 * 60,
+}
+
+var periodOrder = []Period{PeriodDay, PeriodWeek, PeriodMonth, PeriodQuarter, PeriodYear}
+
+type TimeUnit struct {
+ Minutes float64
+ Formatted string
+}
+
+type AvailabilityCalculator struct {
+ availability float64
+}
+
+func NewAvailabilityCalculator(availabilityPercentage float64) (*AvailabilityCalculator, error) {
+ if availabilityPercentage < 0 || availabilityPercentage > 100 {
+ return nil, fmt.Errorf("availability must be between 0 and 100")
+ }
+ return &AvailabilityCalculator{
+ availability: availabilityPercentage / 100.0,
+ }, nil
+}
+
+func (ac *AvailabilityCalculator) CalculateDowntime(period Period) TimeUnit {
+ totalMinutes := periodMinutes[period]
+ downtimeMinutes := totalMinutes * (1 - ac.availability)
+ return TimeUnit{
+ Minutes: downtimeMinutes,
+ Formatted: formatDuration(downtimeMinutes),
+ }
+}
+
+func (ac *AvailabilityCalculator) CalculateAllPeriods() map[Period]TimeUnit {
+ result := make(map[Period]TimeUnit)
+ for _, period := range periodOrder {
+ result[period] = ac.CalculateDowntime(period)
+ }
+ return result
+}
+
+func formatDuration(minutes float64) string {
+ if minutes < 1 {
+ return fmt.Sprintf("%.1f seconds", minutes*60)
+ }
+
+ hours := int(minutes / 60)
+ remainingMinutes := minutes - float64(hours*60)
+
+ if hours == 0 {
+ return fmt.Sprintf("%.1f minutes", remainingMinutes)
+ } else if remainingMinutes == 0 {
+ return fmt.Sprintf("%d hours", hours)
+ } else {
+ return fmt.Sprintf("%d hours %.1f minutes", hours, remainingMinutes)
+ }
+}
+
+func printTable(headers []string, rows [][]string) {
+ w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
+
+ for i, header := range headers {
+ if i > 0 {
+ _, _ = fmt.Fprint(w, "\t")
+ }
+ _, _ = fmt.Fprint(w, header)
+ }
+ _, _ = fmt.Fprintln(w)
+
+ for _, row := range rows {
+ for i, cell := range row {
+ if i > 0 {
+ _, _ = fmt.Fprint(w, "\t")
+ }
+ _, _ = fmt.Fprint(w, cell)
+ }
+ _, _ = fmt.Fprintln(w)
+ }
+
+ _ = w.Flush()
+}
+
+func analyzeSingle(availability float64, focusPeriod string) error {
+ calc, err := NewAvailabilityCalculator(availability)
+ if err != nil {
+ return err
+ }
+
+ var periods []Period
+ if focusPeriod != "" {
+ periods = []Period{Period(focusPeriod)}
+ } else {
+ periods = periodOrder
+ }
+
+ headers := []string{"Time Period", "Maximum Downtime"}
+ var rows [][]string
+
+ caser := cases.Title(language.English)
+ for _, period := range periods {
+ downtime := calc.CalculateDowntime(period)
+ rows = append(rows, []string{
+ caser.String(string(period)),
+ downtime.Formatted,
+ })
+ }
+
+ printTable(headers, rows)
+ return nil
+}
+
+func compareAvailabilities(availability1, availability2 float64) error {
+ if availability1 == availability2 {
+ return fmt.Errorf("cannot compare identical availability values")
+ }
+
+ calc1, err := NewAvailabilityCalculator(availability1)
+ if err != nil {
+ return err
+ }
+
+ calc2, err := NewAvailabilityCalculator(availability2)
+ if err != nil {
+ return err
+ }
+
+ improvement := ((100 - availability1) - (100 - availability2)) / (100 - availability1) * 100
+ fmt.Printf("Downtime reduction: %.1f%%\n\n", improvement)
+
+ headers := []string{
+ "Time Period",
+ fmt.Sprintf("%.2f%%", availability1),
+ fmt.Sprintf("%.2f%%", availability2),
+ }
+ var rows [][]string
+
+ caser := cases.Title(language.English)
+ for _, period := range periodOrder {
+ downtime1 := calc1.CalculateDowntime(period)
+ downtime2 := calc2.CalculateDowntime(period)
+ rows = append(rows, []string{
+ caser.String(string(period)),
+ downtime1.Formatted,
+ downtime2.Formatted,
+ })
+ }
+
+ printTable(headers, rows)
+ return nil
+}
+
+func printUsage() {
+ fmt.Println("Usage:")
+ fmt.Println(" pviz analyze <availability> - Analyze a single availability target")
+ fmt.Println(" pviz analyze <availability> --focus <period> - Focus on a specific time period")
+ fmt.Println(" pviz compare <availability1> <availability2> - Compare two availability targets")
+ fmt.Println()
+ fmt.Println("Examples:")
+ fmt.Println(" pviz analyze 99.99")
+ fmt.Println(" pviz analyze 99.99 --focus quarter")
+ fmt.Println(" pviz compare 99.9 99.95")
+ fmt.Println()
+ fmt.Println("Valid periods: day, week, month, quarter, year")
+}
+
+func main() {
+ if len(os.Args) < 2 {
+ printUsage()
+ os.Exit(1)
+ }
+
+ command := os.Args[1]
+
+ switch command {
+ case "analyze":
+ if len(os.Args) < 3 {
+ fmt.Fprintf(os.Stderr, "Error: analyze command requires availability argument\n")
+ os.Exit(1)
+ }
+
+ availability, err := strconv.ParseFloat(os.Args[2], 64)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: invalid availability value: %v\n", err)
+ os.Exit(1)
+ }
+
+ var focusPeriod string
+ if len(os.Args) >= 5 && os.Args[3] == "--focus" {
+ focusPeriod = os.Args[4]
+ validPeriod := false
+ for _, p := range periodOrder {
+ if string(p) == focusPeriod {
+ validPeriod = true
+ break
+ }
+ }
+ if !validPeriod {
+ fmt.Fprintf(
+ os.Stderr,
+ "Error: invalid period '%s'. Valid periods: day, week, month, quarter, year\n",
+ focusPeriod,
+ )
+ os.Exit(1)
+ }
+ }
+
+ if err := analyzeSingle(availability, focusPeriod); err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ os.Exit(1)
+ }
+
+ case "compare":
+ if len(os.Args) < 4 {
+ fmt.Fprintf(os.Stderr, "Error: compare command requires two availability arguments\n")
+ os.Exit(1)
+ }
+
+ availability1, err := strconv.ParseFloat(os.Args[2], 64)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: invalid first availability value: %v\n", err)
+ os.Exit(1)
+ }
+
+ availability2, err := strconv.ParseFloat(os.Args[3], 64)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: invalid second availability value: %v\n", err)
+ os.Exit(1)
+ }
+
+ if err := compareAvailabilities(availability1, availability2); err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ os.Exit(1)
+ }
+
+ default:
+ fmt.Fprintf(os.Stderr, "Error: unknown command '%s'\n", command)
+ printUsage()
+ os.Exit(1)
+ }
+}
diff --git a/cmd/seq-stat/main.go b/cmd/seq-stat/main.go
new file mode 100644
index 0000000..75bb0a3
--- /dev/null
+++ b/cmd/seq-stat/main.go
@@ -0,0 +1,115 @@
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+)
+
+var ticks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
+
+func histogram(sequence []float64) []rune {
+ if len(sequence) == 0 {
+ return []rune{}
+ }
+
+ minVal := sequence[0]
+ maxVal := sequence[0]
+
+ for _, v := range sequence {
+ if v < minVal {
+ minVal = v
+ }
+ if v > maxVal {
+ maxVal = v
+ }
+ }
+
+ scale := (maxVal - minVal) * 256 / float64(len(ticks)-1)
+ if scale < 1 {
+ scale = 1
+ }
+
+ result := make([]rune, len(sequence))
+ for i, v := range sequence {
+ index := int(((v - minVal) * 256) / scale)
+ if index >= len(ticks) {
+ index = len(ticks) - 1
+ }
+ result[i] = ticks[index]
+ }
+
+ return result
+}
+
+func parseNumbers(input string) ([]float64, error) {
+ fields := strings.Fields(input)
+ numbers := make([]float64, 0, len(fields))
+
+ for _, field := range fields {
+ num, err := strconv.ParseFloat(field, 64)
+ if err != nil {
+ return nil, fmt.Errorf("invalid number: %s", field)
+ }
+ numbers = append(numbers, num)
+ }
+
+ return numbers, nil
+}
+
+func readFromStdin() ([]float64, error) {
+ var numbers []float64
+ scanner := bufio.NewScanner(os.Stdin)
+
+ for scanner.Scan() {
+ line := strings.TrimSpace(scanner.Text())
+ if line == "" {
+ continue
+ }
+
+ lineNumbers, err := parseNumbers(line)
+ if err != nil {
+ return nil, err
+ }
+ numbers = append(numbers, lineNumbers...)
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, fmt.Errorf("error reading from stdin: %v", err)
+ }
+
+ return numbers, nil
+}
+
+func main() {
+ var numbers []float64
+ var err error
+
+ if len(os.Args) > 1 {
+ numbers = make([]float64, 0, len(os.Args)-1)
+ for _, arg := range os.Args[1:] {
+ num, parseErr := strconv.ParseFloat(arg, 64)
+ if parseErr != nil {
+ fmt.Fprintf(os.Stderr, "Invalid number: %s\n", arg)
+ os.Exit(1)
+ }
+ numbers = append(numbers, num)
+ }
+ } else {
+ numbers, err = readFromStdin()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ os.Exit(1)
+ }
+
+ if len(numbers) == 0 {
+ fmt.Fprintln(os.Stderr, "No numbers provided")
+ os.Exit(1)
+ }
+ }
+
+ h := histogram(numbers)
+ fmt.Println(string(h))
+}
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..6d1a15e
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,142 @@
+{
+ "nodes": {
+ "flake-compat": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1747046372,
+ "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
+ "type": "github"
+ },
+ "original": {
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "type": "github"
+ }
+ },
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "gitignore": {
+ "inputs": {
+ "nixpkgs": [
+ "pre-commit-hooks",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1709087332,
+ "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1755274400,
+ "narHash": "sha256-rTInmnp/xYrfcMZyFMH3kc8oko5zYfxsowaLv1LVobY=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "ad7196ae55c295f53a7d1ec39e4a06d922f3b899",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixos-25.05",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "pre-commit-hooks": {
+ "inputs": {
+ "flake-compat": "flake-compat",
+ "gitignore": "gitignore",
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1755446520,
+ "narHash": "sha256-I0Ok1OGDwc1jPd8cs2VvAYZsHriUVFGIUqW+7uSsOUM=",
+ "owner": "cachix",
+ "repo": "git-hooks.nix",
+ "rev": "4b04db83821b819bbbe32ed0a025b31e7971f22e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "cachix",
+ "repo": "git-hooks.nix",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs",
+ "pre-commit-hooks": "pre-commit-hooks",
+ "treefmt-nix": "treefmt-nix"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ },
+ "treefmt-nix": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1754847726,
+ "narHash": "sha256-2vX8QjO5lRsDbNYvN9hVHXLU6oMl+V/PsmIiJREG4rE=",
+ "owner": "numtide",
+ "repo": "treefmt-nix",
+ "rev": "7d81f6fb2e19bf84f1c65135d1060d829fae2408",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "treefmt-nix",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..0833885
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,135 @@
+{
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
+ flake-utils.url = "github:numtide/flake-utils";
+ treefmt-nix = {
+ url = "github:numtide/treefmt-nix";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ pre-commit-hooks = {
+ url = "github:cachix/git-hooks.nix";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ };
+ outputs =
+ {
+ self,
+ nixpkgs,
+ treefmt-nix,
+ flake-utils,
+ pre-commit-hooks,
+ }:
+ let
+ # Import our packages and overlay
+ overlay = import ./nix/overlay.nix;
+
+ # Import NixOS modules
+ nixosModules = {
+ goget = import ./nix/modules/goget.nix;
+ default = {
+ imports = [
+ ./nix/modules/goget.nix
+ ];
+ };
+ };
+ in
+ {
+ # Export the overlay for others to use
+ overlays.default = overlay;
+
+ # Export NixOS modules
+ inherit nixosModules;
+ nixosModule = nixosModules.default;
+ }
+ // flake-utils.lib.eachDefaultSystem (
+ system:
+ let
+ pkgs = import nixpkgs {
+ inherit system;
+ overlays = [ overlay ];
+ };
+
+ packages = import ./nix/packages { inherit pkgs; };
+
+ treefmtEval = treefmt-nix.lib.evalModule pkgs ./treefmt.nix;
+
+ ciPackages = with pkgs; [
+ go # Need that obviously
+ gofumpt # Go formatter
+ golangci-lint # Local/CI linter
+ gotestsum # Pretty tester
+ goperf # Go performance suite
+ ];
+
+ devPackages = with pkgs; [
+ gopls
+ gotools
+ ];
+ in
+ {
+ packages = packages // {
+ default = packages.goget;
+ };
+
+ formatter = treefmtEval.config.build.wrapper;
+
+ checks = {
+ # Throws an error if any of the source files are not correctly formatted
+ # when you run `nix flake check --print-build-logs`. Useful for CI
+ treefmt = treefmtEval.config.build.check self;
+ pre-commit-check = pre-commit-hooks.lib.${system}.run {
+ src = ./.;
+ hooks = {
+ format = {
+ enable = true;
+ name = "Format with treefmt";
+ pass_filenames = true;
+ entry = "${treefmtEval.config.build.wrapper}/bin/treefmt";
+ stages = [
+ "pre-commit"
+ "pre-push"
+ ];
+ };
+ golangci-lint = {
+ enable = true;
+ package = pkgs.golangci-lint;
+ pass_filenames = true;
+ stages = [ "pre-push" ];
+ };
+ unit-tests-go = {
+ enable = true;
+ name = "Run unit tests";
+ entry = "gotestsum --format testdox ./...";
+ pass_filenames = false;
+ stages = [ "pre-push" ];
+ types = [ "go" ];
+ };
+ };
+ };
+ };
+
+ devShells = {
+ default = pkgs.mkShell {
+ buildInputs =
+ [ ] ++ ciPackages ++ devPackages ++ self.checks.${system}.pre-commit-check.enabledPackages;
+
+ shellHook = ''
+ ${self.checks.${system}.pre-commit-check.shellHook}
+ export GOTOOLCHAIN=local
+ export GOPRIVATE=go.fcuny.net/*
+ '';
+ };
+
+ ci = pkgs.mkShell {
+ buildInputs = [ ] ++ ciPackages;
+ CI = true;
+ shellHook = ''
+ export GOTOOLCHAIN=local
+ export GOPRIVATE=go.fcuny.net/*
+ echo "Entering CI shell. Only essential CI tools available."
+ '';
+ };
+ };
+ }
+ );
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..e506b94
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,20 @@
+module go.fcuny.net/x
+
+go 1.24.5
+
+require (
+ github.com/prometheus/client_golang v1.23.0
+ golang.org/x/mod v0.27.0
+ golang.org/x/text v0.28.0
+)
+
+require (
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/prometheus/client_model v0.6.2 // indirect
+ github.com/prometheus/common v0.65.0 // indirect
+ github.com/prometheus/procfs v0.16.1 // indirect
+ golang.org/x/sys v0.33.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..a74ab67
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,38 @@
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
+github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
+golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
+golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/nix/modules/goget.nix b/nix/modules/goget.nix
new file mode 100644
index 0000000..3ed5e04
--- /dev/null
+++ b/nix/modules/goget.nix
@@ -0,0 +1,66 @@
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}:
+
+with lib;
+
+let
+ cfg = config.services.goget;
+in
+{
+ options.services.goget = {
+ enable = mkEnableOption "goget service";
+
+ package = mkPackageOption pkgs "goget" { };
+
+ port = mkOption {
+ type = types.port;
+ default = 8070;
+ description = "Port to listen on";
+ };
+
+ openFirewall = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether to open the firewall for the goget service";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.services.goget = {
+ description = "goget service";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+ wants = [ "network.target" ];
+
+ serviceConfig = {
+ Type = "exec";
+ DynamicUser = true;
+ ExecStart = "${cfg.package}/bin/goget";
+ Restart = "always";
+ RestartSec = "5";
+
+ # Security settings
+ NoNewPrivileges = true;
+ ProtectSystem = "strict";
+ ProtectHome = true;
+ PrivateTmp = true;
+ ProtectKernelTunables = true;
+ ProtectKernelModules = true;
+ ProtectControlGroups = true;
+ RestrictSUIDSGID = true;
+ RestrictRealtime = true;
+ RestrictNamespaces = true;
+ LockPersonality = true;
+ MemoryDenyWriteExecute = true;
+ };
+ };
+
+ networking.firewall = mkIf cfg.openFirewall {
+ allowedTCPPorts = [ cfg.port ];
+ };
+ };
+}
diff --git a/nix/overlay.nix b/nix/overlay.nix
new file mode 100644
index 0000000..6789627
--- /dev/null
+++ b/nix/overlay.nix
@@ -0,0 +1,5 @@
+final: prev:
+let
+ packages = import ./packages { pkgs = final; };
+in
+packages
diff --git a/nix/packages/default.nix b/nix/packages/default.nix
new file mode 100644
index 0000000..e598fa1
--- /dev/null
+++ b/nix/packages/default.nix
@@ -0,0 +1,5 @@
+{ pkgs }:
+
+{
+ goget = pkgs.callPackage ./goget.nix { };
+}
diff --git a/nix/packages/goget.nix b/nix/packages/goget.nix
new file mode 100644
index 0000000..c767740
--- /dev/null
+++ b/nix/packages/goget.nix
@@ -0,0 +1,31 @@
+{
+ lib,
+ buildGoModule,
+}:
+
+buildGoModule rec {
+ pname = "goget";
+ version = "0.1.0"; # Consider deriving from git tags: version = builtins.substring 0 8 self.rev;
+
+ src = ../..;
+
+ vendorHash = "sha256-pStRgjhjjZdsYSnYMcWNbHSF7CJ3+7ZQradZgBfi5Gw=";
+
+ subPackages = [ "cmd/goget" ];
+
+ ldflags = [
+ "-s"
+ "-w"
+ ];
+
+ doCheck = false;
+
+ meta = with lib; {
+ description = "A Go tool for getting things"; # Update with actual description
+ homepage = "https://github.com/yourusername/yourrepo"; # Update with your repo
+ license = licenses.mit;
+ maintainers = with maintainers; [ fcuny ];
+ platforms = platforms.unix;
+ mainProgram = "goget";
+ };
+}
diff --git a/treefmt.nix b/treefmt.nix
new file mode 100644
index 0000000..6d19624
--- /dev/null
+++ b/treefmt.nix
@@ -0,0 +1,30 @@
+{ pkgs, ... }:
+{
+ # See https://github.com/numtide/treefmt-nix#supported-programs
+ projectRootFile = ".git/config";
+ settings.global.includes = [
+ "*.go"
+ "*.yaml"
+ "*.yml"
+ "*.md"
+ "*.nix"
+ ];
+ settings.global.fail-on-change = true;
+ settings.global.no-cache = true;
+ programs.gofumpt = {
+ enable = true;
+ package = pkgs.gofumpt;
+ };
+ programs.goimports.enable = true;
+ programs.golines.enable = true;
+ # GitHub Actions
+ programs.yamlfmt.enable = true;
+ programs.actionlint.enable = true;
+ # Markdown
+ programs.mdformat.enable = true;
+ # Nix
+ programs.nixfmt = {
+ enable = true;
+ package = pkgs.nixfmt-rfc-style;
+ };
+}