cmd/badges/main.go GO 616 lines View on github.com → Search inside
1package main23import (4	"context"5	"errors"6	"fmt"7	"math"8	"net/http"9	"net/url"10	"os"11	"os/exec"12	"path/filepath"13	"regexp"14	"slices"15	"strconv"16	"strings"17	"sync"18	"time"1920	"github.com/boyter/scc/v3/processor"21	"github.com/boyter/simplecache"22	jsoniter "github.com/json-iterator/go"23	"github.com/rs/zerolog/log"24)2526var (27	uniqueCode = "unique_code"28	cache      = simplecache.New[[]processor.LanguageSummary](simplecache.Option{29		MaxItems: intPtr(1000),30		MaxAge:   timePtr(time.Hour * 72),31	})32	countingSemaphore = make(chan bool, 1)33	tmpDir            = os.TempDir()34	json              = jsoniter.ConfigCompatibleWithStandardLibrary35	locationLog       = []string{}36	locationTracker   = map[string]int{}37	locationLogMutex  = sync.Mutex{}38)3940func intPtr(i int) *int {41	return &i42}4344func timePtr(t time.Duration) *time.Duration {45	return &t46}4748func main() {49	http.HandleFunc("/health-check/", func(w http.ResponseWriter, r *http.Request) {50		locationLogMutex.Lock()51		for k, v := range locationTracker {52			_, _ = fmt.Fprintf(w, "%s:%d\n", k, v)53		}54		locationLogMutex.Unlock()55	})5657	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {58		loc, err := processUrlPath(r.URL.Path)59		if err != nil {60			http.Redirect(w, r, "https://github.com/boyter/scc/?tab=readme-ov-file#badges-beta", http.StatusTemporaryRedirect)61			return62		}6364		if filterBad(loc) {65			log.Error().Str(uniqueCode, "bfee4bd8").Str("loc", loc.String()).Msg("filter bad")66			return67		}6869		appendLocationLog(loc.String())7071		res, err := process(1, loc)72		if err != nil {73			log.Error().Str(uniqueCode, "03ec75c3").Err(err).Str("loc", loc.String()).Send()74			w.WriteHeader(http.StatusBadRequest)75			_, _ = w.Write([]byte("something bad happened sorry"))76			return77		}7879		category := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("category")))80		wage := tryParseInt(strings.TrimSpace(strings.ToLower(r.URL.Query().Get("avg-wage"))), 56286)81		title, value := calculate(category, wage, res)8283		if r.URL.Query().Get("lower") != "" {84			title = strings.ToLower(title)85		}8687		s := formatCount(float64(value))88		if category == "effort" {89			s = formatMonthsInt(value)90		}9192		textLength := "250"93		if len(s) <= 3 {94			textLength = "200"95		}9697		bs := parseBadgeSettings(r.URL.Query())9899		log.Info().Str(uniqueCode, "42c5269c").Str("loc", loc.String()).Str("category", category).Send()100		w.Header().Set("Content-Type", "image/svg+xml;charset=utf-8")101		if category == "effort" {102			_, _ = w.Write([]byte(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="240" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="` + bs.TopShadowAccentColor + `" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="240" height="20" rx="3" fill="#` + bs.FontColor + `"/></clipPath><g clip-path="url(#a)"><path fill="#` + bs.TitleBackgroundColor + `" d="M0 0h125v20H0z"/><path fill="#` + bs.BadgeBackgroundColor + `" d="M125 0h115v20H125z"/><path fill="url(#b)" d="M0 0h240v20H0z"/></g><g fill="#` + bs.FontColor + `" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"> <text x="625" y="150" fill="#` + bs.TextShadowColor + `" fill-opacity=".3" transform="scale(.1)">` + title + `</text><text x="625" y="140" transform="scale(.1)">` + title + `</text><text x="1825" y="150" fill="#` + bs.TextShadowColor + `" fill-opacity=".3" transform="scale(.1)">` + s + `</text><text x="1825" y="140" transform="scale(.1)">` + s + `</text></g> </svg>`))103		} else {104			_, _ = w.Write([]byte(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="` + bs.TopShadowAccentColor + `" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="100" height="20" rx="3" fill="#` + bs.FontColor + `"/></clipPath><g clip-path="url(#a)"><path fill="#` + bs.TitleBackgroundColor + `" d="M0 0h69v20H0z"/><path fill="#` + bs.BadgeBackgroundColor + `" d="M69 0h31v20H69z"/><path fill="url(#b)" d="M0 0h100v20H0z"/></g><g fill="#` + bs.FontColor + `" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"> <text x="355" y="150" fill="#` + bs.TextShadowColor + `" fill-opacity=".3" transform="scale(.1)" textLength="590">` + title + `</text><text x="355" y="140" transform="scale(.1)" textLength="590">` + title + `</text><text x="835" y="150" fill="#` + bs.TextShadowColor + `" fill-opacity=".3" transform="scale(.1)" textLength="` + textLength + `">` + s + `</text><text x="835" y="140" transform="scale(.1)" textLength="` + textLength + `">` + s + `</text></g> </svg>`))105		}106	})107108	addr := ":8080"109	log.Info().Str(uniqueCode, "1876ce1e").Str("addr", addr).Msg("serving")110	if err := http.ListenAndServe(addr, nil); err != nil && !errors.Is(err, http.ErrServerClosed) {111		log.Error().Str(uniqueCode, "c28556e8").Err(err).Send()112		os.Exit(1)113	}114}115116func filterBad(loc location) bool {117	l := loc.String()118	bad := []string{"wp-content.com", "wp-admin.com", ".well-known", "wp-includes.com", ".php"}119	count := 0120	for _, b := range bad {121		if strings.Contains(l, b) {122			count++123		}124	}125126	return count >= 2127}128129func appendLocationLog(log string) {130	locationLogMutex.Lock()131	defer locationLogMutex.Unlock()132133	if slices.Contains(locationLog, log) {134		return135	}136	locationLog = append(locationLog, log)137	locationTracker[log] = locationTracker[log] + 1138139	if len(locationLog) > 100 {140		locationLog = locationLog[1:]141	}142}143144func calculate(category string, wage int, res []processor.LanguageSummary) (string, int64) {145	title := ""146	var value int64147148	switch category {149	case "code", "codes":150		title = "Code lines"151		for _, x := range res {152			value += x.Code153		}154	case "blank", "blanks":155		title = "Blank lines"156		for _, x := range res {157			value += x.Blank158		}159	case "comment", "comments":160		title = "Comments"161		for _, x := range res {162			value += x.Comment163		}164	case "cocomo":165		title = "COCOMO $"166		for _, x := range res {167			value += x.Code168		}169170		value = int64(estimateCost(value, wage))171	case "effort":172		title = "COCOMO Man Years"173		for _, x := range res {174			value += x.Code175		}176177		value = int64(estimateScheduleMonths(value))178	case "line", "lines": // lines is the default179		fallthrough180	default:181		//182		title = "Total lines"183		for _, x := range res {184			value += x.Lines185		}186	}187	return title, value188}189190type location struct {191	Provider string192	User     string193	Repo     string194}195196func (l *location) String() string {197	loc := ".com/"198	ext := ".git"199	switch strings.ToLower(l.Provider) {200	case "bitbucket":201		loc = ".org/"202	case "git.sr.ht":203		loc = "/"204		ext = ""205	}206207	parse, _ := url.Parse("https://" + l.Provider + loc + l.User + "/" + l.Repo + ext)208	return parse.String()209}210211// processUrlPath takes in a string path and returns a struct212// that contains the location user and repo which is what most213// repositories need214// returns an error if we get anything other than 3 parts since thats215// the format we expect216func processUrlPath(path string) (location, error) {217	path = strings.ToLower(path)218	path = strings.TrimPrefix(path, "/")219	path = strings.TrimSuffix(path, "/")220	s := strings.Split(path, "/")221	if len(s) != 3 {222		return location{}, errors.New("invalid path part")223	}224225	if s[0] == "sr.ht" {226		s[0] = "git.sr.ht"227	}228229	return location{230		Provider: s[0],231		User:     s[1],232		Repo:     s[2],233	}, nil234}235236type badgeSettings struct {237	FontColor            string238	TextShadowColor      string239	TopShadowAccentColor string240	TitleBackgroundColor string241	BadgeBackgroundColor string242}243244// namedColors maps color names to their hex values (without #).245// Includes shields.io colors and common CSS color names for compatibility.246var namedColors = map[string]string{247	// shields.io specific colors248	"brightgreen": "44cc11",249	"green":       "97ca00",250	"yellowgreen": "a4a61d",251	"yellow":      "dfb317",252	"orange":      "fe7d37",253	"red":         "e05d44",254	"blue":        "007ec6",255	"lightgrey":   "9f9f9f",256	"lightgray":   "9f9f9f",257	"grey":        "555555",258	"gray":        "555555",259	"blueviolet":  "8a2be2",260	// shields.io semantic aliases261	"success":       "44cc11",262	"important":     "fe7d37",263	"critical":      "e05d44",264	"informational": "007ec6",265	"inactive":      "9f9f9f",266	// Common CSS color names267	"black":           "000000",268	"white":           "ffffff",269	"silver":          "c0c0c0",270	"maroon":          "800000",271	"purple":          "800080",272	"fuchsia":         "ff00ff",273	"lime":            "00ff00",274	"olive":           "808000",275	"navy":            "000080",276	"teal":            "008080",277	"aqua":            "00ffff",278	"cyan":            "00ffff",279	"magenta":         "ff00ff",280	"pink":            "ffc0cb",281	"coral":           "ff7f50",282	"salmon":          "fa8072",283	"gold":            "ffd700",284	"khaki":           "f0e68c",285	"violet":          "ee82ee",286	"indigo":          "4b0082",287	"crimson":         "dc143c",288	"turquoise":       "40e0d0",289	"tan":             "d2b48c",290	"brown":           "a52a2a",291	"chocolate":       "d2691e",292	"tomato":          "ff6347",293	"orchid":          "da70d6",294	"plum":            "dda0dd",295	"peru":            "cd853f",296	"sienna":          "a0522d",297	"beige":           "f5f5dc",298	"ivory":           "fffff0",299	"linen":           "faf0e6",300	"azure":           "f0ffff",301	"lavender":        "e6e6fa",302	"wheat":           "f5deb3",303	"snow":            "fffafa",304	"seashell":        "fff5ee",305	"honeydew":        "f0fff0",306	"mintcream":       "f5fffa",307	"aliceblue":       "f0f8ff",308	"ghostwhite":      "f8f8ff",309	"oldlace":         "fdf5e6",310	"papayawhip":      "ffefd5",311	"moccasin":        "ffe4b5",312	"bisque":          "ffe4c4",313	"mistyrose":       "ffe4e1",314	"lemonchiffon":    "fffacd",315	"cornsilk":        "fff8dc",316	"antiquewhite":    "faebd7",317	"floralwhite":     "fffaf0",318	"steelblue":       "4682b4",319	"royalblue":       "4169e1",320	"skyblue":         "87ceeb",321	"dodgerblue":      "1e90ff",322	"deepskyblue":     "00bfff",323	"cadetblue":       "5f9ea0",324	"cornflowerblue":  "6495ed",325	"mediumblue":      "0000cd",326	"darkblue":        "00008b",327	"midnightblue":    "191970",328	"slateblue":       "6a5acd",329	"darkslateblue":   "483d8b",330	"mediumslateblue": "7b68ee",331	"seagreen":        "2e8b57",332	"mediumseagreen":  "3cb371",333	"lightgreen":      "90ee90",334	"darkgreen":       "006400",335	"forestgreen":     "228b22",336	"limegreen":       "32cd32",337	"springgreen":     "00ff7f",338	"palegreen":       "98fb98",339	"darkseagreen":    "8fbc8f",340	"olivedrab":       "6b8e23",341	"darkolivegreen":  "556b2f",342	"darkred":         "8b0000",343	"firebrick":       "b22222",344	"indianred":       "cd5c5c",345	"lightsalmon":     "ffa07a",346	"darksalmon":      "e9967a",347	"lightcoral":      "f08080",348	"rosybrown":       "bc8f8f",349	"sandybrown":      "f4a460",350	"goldenrod":       "daa520",351	"darkgoldenrod":   "b8860b",352	"darkorange":      "ff8c00",353	"orangered":       "ff4500",354	"hotpink":         "ff69b4",355	"deeppink":        "ff1493",356	"palevioletred":   "db7093",357	"mediumvioletred": "c71585",358	"mediumpurple":    "9370db",359	"darkorchid":      "9932cc",360	"darkviolet":      "9400d3",361	"darkmagenta":     "8b008b",362	"slategray":       "708090",363	"slategrey":       "708090",364	"lightslategray":  "778899",365	"lightslategrey":  "778899",366	"darkslategray":   "2f4f4f",367	"darkslategrey":   "2f4f4f",368	"dimgray":         "696969",369	"dimgrey":         "696969",370	"darkgray":        "a9a9a9",371	"darkgrey":        "a9a9a9",372	"gainsboro":       "dcdcdc",373	"whitesmoke":      "f5f5f5",374}375376// resolveColor converts a color input (name or hex) to a hex value without #.377// Returns the hex value if valid, or empty string if invalid.378func resolveColor(color string) string {379	color = strings.ToLower(color)380381	// Check if it's a named color382	if hex, ok := namedColors[color]; ok {383		return hex384	}385386	// Check if it's a valid hex color (3, 4, 6, or 8 digits)387	hexRegex := regexp.MustCompile(`^(?:(?:[\da-f]{3}){1,2}|(?:[\da-f]{4}){1,2})$`)388	if hexRegex.MatchString(color) {389		return color390	}391392	return ""393}394395// Parses badge settings from url query params396// if error, ignore and return default badge settings397func parseBadgeSettings(values url.Values) *badgeSettings {398	bs := badgeSettings{399		FontColor:            "fff",400		TextShadowColor:      "010101",401		TopShadowAccentColor: "bbb",402		TitleBackgroundColor: "555",403		BadgeBackgroundColor: "4c1",404	}405406	fontColor := values.Get("font-color")407	textShadowColor := values.Get("font-shadow-color")408	topShadowAccentColor := values.Get("top-shadow-accent-color")409	titleBackgroundColor := values.Get("title-bg-color")410	badgeBackgroundColor := values.Get("badge-bg-color")411412	// Resolve colors (supports both named colors and hex codes)413	if resolved := resolveColor(fontColor); resolved != "" {414		bs.FontColor = resolved415	}416	if resolved := resolveColor(textShadowColor); resolved != "" {417		bs.TextShadowColor = resolved418	}419	if resolved := resolveColor(topShadowAccentColor); resolved != "" {420		bs.TopShadowAccentColor = resolved421	}422	if resolved := resolveColor(titleBackgroundColor); resolved != "" {423		bs.TitleBackgroundColor = resolved424	}425	if resolved := resolveColor(badgeBackgroundColor); resolved != "" {426		bs.BadgeBackgroundColor = resolved427	}428429	return &bs430}431432// formatCount turns a float into a string usable for display433// to the user so, 2532 would be 2.5k and such up the various434// units435func formatCount(count float64) string {436	type r struct {437		val float64438		sym string439	}440	ranges := []r{441		{1e18, "E"},442		{1e15, "P"},443		{1e12, "T"},444		{1e9, "G"},445		{1e6, "M"},446		{1e3, "k"},447	}448449	for _, v := range ranges {450		if count >= v.val {451			t := fmt.Sprintf("%.1f", math.Ceil(count/v.val*10)/10)452453			if len(t) > 3 {454				t = t[:strings.Index(t, ".")]455			}456457			return fmt.Sprintf("%v%v", t, v.sym)458		}459	}460461	return fmt.Sprintf("%v", math.Round(count))462}463464func process(id int, s location) ([]processor.LanguageSummary, error) {465	countingSemaphore <- true466	defer func() {467		<-countingSemaphore // remove one to free up concurrency468	}()469470	val, ok := cache.Get(s.String())471	if ok {472		return val, nil473	}474475	// Clean target just to be sure476	targetPath := filepath.Join(tmpDir, "scc-tmp-path-"+strconv.Itoa(id))477	if err := os.RemoveAll(targetPath); err != nil {478		return nil, err479	}480481	// Run git clone against the target482	// 180 seconds seems enough as the kernel itself takes about 60 seconds483	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)484	defer cancel()485486	cmd := exec.CommandContext(ctx, "git", "clone", "--depth=1", s.String(), targetPath)487488	cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")489	_, err := cmd.Output()490491	if errors.Is(ctx.Err(), context.DeadlineExceeded) {492		return nil, err493	}494495	if err != nil {496		return nil, err497	}498499	// Run scc against what we just cloned500	fileName := processPath(s.String())501	filePath := filepath.Join(tmpDir, fileName)502503	if fileName == "" {504		return nil, errors.New("processPath returned empty")505	}506507	cmdArgs := []string{508		"-f",509		"json",510		"-o",511		filePath,512		targetPath,513	}514515	cmd = exec.Command("scc", cmdArgs...)516	err = cmd.Run()517	if err != nil {518		return nil, err519	}520521	data, err := os.ReadFile(filePath)522	if err != nil {523		return nil, err524	}525526	var res []processor.LanguageSummary527	err = json.Unmarshal(data, &res)528	if err != nil {529		return nil, err530	}531	_ = cache.Set(s.String(), res)532533	// Cleanup534	if err := os.RemoveAll(filePath); err != nil {535		return nil, err536	}537538	if err := os.RemoveAll(targetPath); err != nil {539		return nil, err540	}541542	return res, nil543}544545func processPath(s string) string {546	s = strings.ToLower(s)547	split := strings.Split(s, "/")548549	if len(split) != 5 {550		return ""551	}552553	sp := make([]string, 0, len(split))554555	for _, s := range split {556		sp = append(sp, cleanString(s))557	}558559	filename := strings.ReplaceAll(sp[2], ".com", "")560	filename = strings.ReplaceAll(filename, ".org", "")561	filename += "." + sp[3]562	filename += "." + strings.ReplaceAll(sp[4], ".git", "") + ".json"563564	return filename565}566567func cleanString(s string) string {568	reg, err := regexp.Compile("[^a-z0-9-._]+")569	if err != nil {570		log.Fatal().Err(err).Send()571	}572573	processedString := reg.ReplaceAllString(s, "")574575	return processedString576}577578func estimateEffort(codeCount int64) float64 {579	return 3.2 * math.Pow(float64(codeCount)/1000, 1.05) * 1580}581582func estimateCost(codeCount int64, averageWage int) float64 {583	return estimateEffort(codeCount) * (float64(averageWage) / 12) * 1.8584}585586func estimateScheduleMonths(codeCount int64) float64 {587	return 2.5 * math.Pow(estimateEffort(codeCount), 0.38)588}589590// FormatMonths converts a float64 representing months into a "X years Y months" string.591func formatMonths(totalMonths float64) string {592	years, fracMonths := math.Modf(totalMonths / 12)593	months := int(math.Round(fracMonths * 12))594595	return fmt.Sprintf("%d years %d months", int(years), months)596}597598// FormatMonthsInt converts an int64 representing months into a "X years Y months" string.599func formatMonthsInt(totalMonths int64) string {600	// Use integer division to get the number of years601	years := totalMonths / 12602603	// Use the modulo operator to get the remaining months604	months := totalMonths % 12605606	return fmt.Sprintf("%d years %d months", years, months)607}608609func tryParseInt(s string, def int) int {610	i, err := strconv.Atoi(s)611	if err != nil {612		return def613	}614	return i615}

Code quality findings 9

Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(w, "%s:%d\n", k, v)
Declared map variable without initialization; writing to a nil map causes a panic. Use make() to initialize
warning correctness nil-map-write
var namedColors = map[string]string{
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_ = cache.Set(s.String(), res)
Ensure errors are handled or logged
warning correctness unhandled-error
if err != nil {
Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for k, v := range locationTracker {
Regexp compiled inside function; compile once at package level to avoid recompilation on each call
info performance regexp-compile-in-func
hexRegex := regexp.MustCompile(`^(?:(?:[\da-f]{3}){1,2}|(?:[\da-f]{4}){1,2})$`)
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
targetPath := filepath.Join(tmpDir, "scc-tmp-path-"+strconv.Itoa(id))
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
filePath := filepath.Join(tmpDir, fileName)
Regexp compiled inside function; compile once at package level to avoid recompilation on each call
info performance regexp-compile-in-func
reg, err := regexp.Compile("[^a-z0-9-._]+")

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.