Ensure errors are handled or logged
if err != nil {
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/v4/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 w.WriteHeader(http.StatusBadRequest)67 _, _ = w.Write([]byte("invalid request"))68 return69 }7071 appendLocationLog(loc.String())7273 res, err := process(1, loc)74 if err != nil {75 log.Error().Str(uniqueCode, "03ec75c3").Err(err).Str("loc", loc.String()).Send()76 w.WriteHeader(http.StatusBadRequest)77 _, _ = w.Write([]byte("something bad happened sorry"))78 return79 }8081 category := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("category")))82 wage := tryParseInt(strings.TrimSpace(strings.ToLower(r.URL.Query().Get("avg-wage"))), 56286)83 title, value := calculate(category, wage, res)8485 if r.URL.Query().Get("lower") != "" {86 title = strings.ToLower(title)87 }8889 s := formatCount(float64(value))90 if category == "effort" {91 s = formatMonthsInt(value)92 }9394 textLength := "250"95 if len(s) <= 3 {96 textLength = "200"97 }9899 bs := parseBadgeSettings(r.URL.Query())100101 log.Info().Str(uniqueCode, "42c5269c").Str("loc", loc.String()).Str("category", category).Send()102 w.Header().Set("Content-Type", "image/svg+xml;charset=utf-8")103 if category == "effort" {104 _, _ = 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>`))105 } else {106 _, _ = 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>`))107 }108 })109110 addr := ":8080"111 log.Info().Str(uniqueCode, "1876ce1e").Str("addr", addr).Msg("serving")112 if err := http.ListenAndServe(addr, nil); err != nil && !errors.Is(err, http.ErrServerClosed) {113 log.Error().Str(uniqueCode, "c28556e8").Err(err).Send()114 os.Exit(1)115 }116}117118func filterBad(loc location) bool {119 l := loc.String()120 bad := []string{"wp-content.com", "wp-admin.com", ".well-known", "wp-includes.com", ".php"}121 count := 0122 for _, b := range bad {123 if strings.Contains(l, b) {124 count++125 }126 }127128 return count >= 2129}130131func appendLocationLog(log string) {132 locationLogMutex.Lock()133 defer locationLogMutex.Unlock()134135 if slices.Contains(locationLog, log) {136 return137 }138 locationLog = append(locationLog, log)139 locationTracker[log] = locationTracker[log] + 1140141 if len(locationLog) > 100 {142 locationLog = locationLog[1:]143 }144}145146func calculate(category string, wage int, res []processor.LanguageSummary) (string, int64) {147 title := ""148 var value int64149150 switch category {151 case "code", "codes":152 title = "Code lines"153 for _, x := range res {154 value += x.Code155 }156 case "blank", "blanks":157 title = "Blank lines"158 for _, x := range res {159 value += x.Blank160 }161 case "comment", "comments":162 title = "Comments"163 for _, x := range res {164 value += x.Comment165 }166 case "cocomo":167 title = "COCOMO $"168 for _, x := range res {169 value += x.Code170 }171172 value = int64(estimateCost(value, wage))173 case "effort":174 title = "COCOMO Man Years"175 for _, x := range res {176 value += x.Code177 }178179 value = int64(estimateScheduleMonths(value))180 case "line", "lines": // lines is the default181 fallthrough182 default:183 //184 title = "Total lines"185 for _, x := range res {186 value += x.Lines187 }188 }189 return title, value190}191192type location struct {193 Provider string194 User string195 Repo string196}197198func (l *location) String() string {199 loc := ".com/"200 ext := ".git"201 switch strings.ToLower(l.Provider) {202 case "bitbucket", "codeberg":203 loc = ".org/"204 case "git.sr.ht":205 loc = "/"206 ext = ""207 }208209 rawURL := "https://" + l.Provider + loc + l.User + "/" + l.Repo + ext210 parse, err := url.Parse(rawURL)211 if err != nil {212 return rawURL213 }214 return parse.String()215}216217// processUrlPath takes in a string path and returns a struct218// that contains the location user and repo which is what most219// repositories need220// returns an error if we get anything other than 3 parts since thats221// the format we expect222func processUrlPath(path string) (location, error) {223 path = strings.ToLower(path)224 path = strings.TrimPrefix(path, "/")225 path = strings.TrimSuffix(path, "/")226 s := strings.Split(path, "/")227 if len(s) != 3 {228 return location{}, errors.New("invalid path part")229 }230231 if s[0] == "sr.ht" {232 s[0] = "git.sr.ht"233 }234235 return location{236 Provider: s[0],237 User: s[1],238 Repo: s[2],239 }, nil240}241242type badgeSettings struct {243 FontColor string244 TextShadowColor string245 TopShadowAccentColor string246 TitleBackgroundColor string247 BadgeBackgroundColor string248}249250// namedColors maps color names to their hex values (without #).251// Includes shields.io colors and common CSS color names for compatibility.252var namedColors = map[string]string{253 // shields.io specific colors254 "brightgreen": "44cc11",255 "green": "97ca00",256 "yellowgreen": "a4a61d",257 "yellow": "dfb317",258 "orange": "fe7d37",259 "red": "e05d44",260 "blue": "007ec6",261 "lightgrey": "9f9f9f",262 "lightgray": "9f9f9f",263 "grey": "555555",264 "gray": "555555",265 "blueviolet": "8a2be2",266 // shields.io semantic aliases267 "success": "44cc11",268 "important": "fe7d37",269 "critical": "e05d44",270 "informational": "007ec6",271 "inactive": "9f9f9f",272 // Common CSS color names273 "black": "000000",274 "white": "ffffff",275 "silver": "c0c0c0",276 "maroon": "800000",277 "purple": "800080",278 "fuchsia": "ff00ff",279 "lime": "00ff00",280 "olive": "808000",281 "navy": "000080",282 "teal": "008080",283 "aqua": "00ffff",284 "cyan": "00ffff",285 "magenta": "ff00ff",286 "pink": "ffc0cb",287 "coral": "ff7f50",288 "salmon": "fa8072",289 "gold": "ffd700",290 "khaki": "f0e68c",291 "violet": "ee82ee",292 "indigo": "4b0082",293 "crimson": "dc143c",294 "turquoise": "40e0d0",295 "tan": "d2b48c",296 "brown": "a52a2a",297 "chocolate": "d2691e",298 "tomato": "ff6347",299 "orchid": "da70d6",300 "plum": "dda0dd",301 "peru": "cd853f",302 "sienna": "a0522d",303 "beige": "f5f5dc",304 "ivory": "fffff0",305 "linen": "faf0e6",306 "azure": "f0ffff",307 "lavender": "e6e6fa",308 "wheat": "f5deb3",309 "snow": "fffafa",310 "seashell": "fff5ee",311 "honeydew": "f0fff0",312 "mintcream": "f5fffa",313 "aliceblue": "f0f8ff",314 "ghostwhite": "f8f8ff",315 "oldlace": "fdf5e6",316 "papayawhip": "ffefd5",317 "moccasin": "ffe4b5",318 "bisque": "ffe4c4",319 "mistyrose": "ffe4e1",320 "lemonchiffon": "fffacd",321 "cornsilk": "fff8dc",322 "antiquewhite": "faebd7",323 "floralwhite": "fffaf0",324 "steelblue": "4682b4",325 "royalblue": "4169e1",326 "skyblue": "87ceeb",327 "dodgerblue": "1e90ff",328 "deepskyblue": "00bfff",329 "cadetblue": "5f9ea0",330 "cornflowerblue": "6495ed",331 "mediumblue": "0000cd",332 "darkblue": "00008b",333 "midnightblue": "191970",334 "slateblue": "6a5acd",335 "darkslateblue": "483d8b",336 "mediumslateblue": "7b68ee",337 "seagreen": "2e8b57",338 "mediumseagreen": "3cb371",339 "lightgreen": "90ee90",340 "darkgreen": "006400",341 "forestgreen": "228b22",342 "limegreen": "32cd32",343 "springgreen": "00ff7f",344 "palegreen": "98fb98",345 "darkseagreen": "8fbc8f",346 "olivedrab": "6b8e23",347 "darkolivegreen": "556b2f",348 "darkred": "8b0000",349 "firebrick": "b22222",350 "indianred": "cd5c5c",351 "lightsalmon": "ffa07a",352 "darksalmon": "e9967a",353 "lightcoral": "f08080",354 "rosybrown": "bc8f8f",355 "sandybrown": "f4a460",356 "goldenrod": "daa520",357 "darkgoldenrod": "b8860b",358 "darkorange": "ff8c00",359 "orangered": "ff4500",360 "hotpink": "ff69b4",361 "deeppink": "ff1493",362 "palevioletred": "db7093",363 "mediumvioletred": "c71585",364 "mediumpurple": "9370db",365 "darkorchid": "9932cc",366 "darkviolet": "9400d3",367 "darkmagenta": "8b008b",368 "slategray": "708090",369 "slategrey": "708090",370 "lightslategray": "778899",371 "lightslategrey": "778899",372 "darkslategray": "2f4f4f",373 "darkslategrey": "2f4f4f",374 "dimgray": "696969",375 "dimgrey": "696969",376 "darkgray": "a9a9a9",377 "darkgrey": "a9a9a9",378 "gainsboro": "dcdcdc",379 "whitesmoke": "f5f5f5",380}381382// resolveColor converts a color input (name or hex) to a hex value without #.383// Returns the hex value if valid, or empty string if invalid.384func resolveColor(color string) string {385 color = strings.ToLower(color)386387 // Check if it's a named color388 if hex, ok := namedColors[color]; ok {389 return hex390 }391392 // Check if it's a valid hex color (3, 4, 6, or 8 digits)393 hexRegex := regexp.MustCompile(`^(?:(?:[\da-f]{3}){1,2}|(?:[\da-f]{4}){1,2})$`)394 if hexRegex.MatchString(color) {395 return color396 }397398 return ""399}400401// Parses badge settings from url query params402// if error, ignore and return default badge settings403func parseBadgeSettings(values url.Values) *badgeSettings {404 bs := badgeSettings{405 FontColor: "fff",406 TextShadowColor: "010101",407 TopShadowAccentColor: "bbb",408 TitleBackgroundColor: "555",409 BadgeBackgroundColor: "4c1",410 }411412 fontColor := values.Get("font-color")413 textShadowColor := values.Get("font-shadow-color")414 topShadowAccentColor := values.Get("top-shadow-accent-color")415 titleBackgroundColor := values.Get("title-bg-color")416 badgeBackgroundColor := values.Get("badge-bg-color")417418 // Resolve colors (supports both named colors and hex codes)419 if resolved := resolveColor(fontColor); resolved != "" {420 bs.FontColor = resolved421 }422 if resolved := resolveColor(textShadowColor); resolved != "" {423 bs.TextShadowColor = resolved424 }425 if resolved := resolveColor(topShadowAccentColor); resolved != "" {426 bs.TopShadowAccentColor = resolved427 }428 if resolved := resolveColor(titleBackgroundColor); resolved != "" {429 bs.TitleBackgroundColor = resolved430 }431 if resolved := resolveColor(badgeBackgroundColor); resolved != "" {432 bs.BadgeBackgroundColor = resolved433 }434435 return &bs436}437438// formatCount turns a float into a string usable for display439// to the user so, 2532 would be 2.5k and such up the various440// units441func formatCount(count float64) string {442 type r struct {443 val float64444 sym string445 }446 ranges := []r{447 {1e18, "E"},448 {1e15, "P"},449 {1e12, "T"},450 {1e9, "G"},451 {1e6, "M"},452 {1e3, "k"},453 }454455 for _, v := range ranges {456 if count >= v.val {457 t := fmt.Sprintf("%.1f", math.Ceil(count/v.val*10)/10)458459 if len(t) > 3 {460 t = t[:strings.Index(t, ".")]461 }462463 return fmt.Sprintf("%v%v", t, v.sym)464 }465 }466467 return fmt.Sprintf("%v", math.Round(count))468}469470func process(id int, s location) ([]processor.LanguageSummary, error) {471 countingSemaphore <- true472 defer func() {473 <-countingSemaphore // remove one to free up concurrency474 }()475476 val, ok := cache.Get(s.String())477 if ok {478 return val, nil479 }480481 // Clean target just to be sure482 targetPath := filepath.Join(tmpDir, "scc-tmp-path-"+strconv.Itoa(id))483 if err := os.RemoveAll(targetPath); err != nil {484 return nil, err485 }486487 // Run git clone against the target488 // 180 seconds seems enough as the kernel itself takes about 60 seconds489 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)490 defer cancel()491492 cmd := exec.CommandContext(ctx, "git", "clone", "--depth=1", s.String(), targetPath)493494 cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")495 _, err := cmd.Output()496497 if errors.Is(ctx.Err(), context.DeadlineExceeded) {498 return nil, err499 }500501 if err != nil {502 return nil, err503 }504505 // Run scc against what we just cloned506 fileName := processPath(s.String())507 filePath := filepath.Join(tmpDir, fileName)508509 if fileName == "" {510 return nil, errors.New("processPath returned empty")511 }512513 cmdArgs := []string{514 "-f",515 "json",516 "-o",517 filePath,518 targetPath,519 }520521 cmd = exec.Command("scc", cmdArgs...)522 err = cmd.Run()523 if err != nil {524 return nil, err525 }526527 data, err := os.ReadFile(filePath)528 if err != nil {529 return nil, err530 }531532 var res []processor.LanguageSummary533 err = json.Unmarshal(data, &res)534 if err != nil {535 return nil, err536 }537 _ = cache.Set(s.String(), res)538539 // Cleanup540 if err := os.RemoveAll(filePath); err != nil {541 return nil, err542 }543544 if err := os.RemoveAll(targetPath); err != nil {545 return nil, err546 }547548 return res, nil549}550551func processPath(s string) string {552 s = strings.ToLower(s)553 split := strings.Split(s, "/")554555 if len(split) != 5 {556 return ""557 }558559 sp := make([]string, 0, len(split))560561 for _, s := range split {562 sp = append(sp, cleanString(s))563 }564565 filename := strings.ReplaceAll(sp[2], ".com", "")566 filename = strings.ReplaceAll(filename, ".org", "")567 filename += "." + sp[3]568 filename += "." + strings.ReplaceAll(sp[4], ".git", "") + ".json"569570 return filename571}572573func cleanString(s string) string {574 reg, err := regexp.Compile("[^a-z0-9-._]+")575 if err != nil {576 log.Fatal().Err(err).Send()577 }578579 processedString := reg.ReplaceAllString(s, "")580581 return processedString582}583584func estimateEffort(codeCount int64) float64 {585 return 3.2 * math.Pow(float64(codeCount)/1000, 1.05) * 1586}587588func estimateCost(codeCount int64, averageWage int) float64 {589 return estimateEffort(codeCount) * (float64(averageWage) / 12) * 1.8590}591592func estimateScheduleMonths(codeCount int64) float64 {593 return 2.5 * math.Pow(estimateEffort(codeCount), 0.38)594}595596// FormatMonths converts a float64 representing months into a "X years Y months" string.597func formatMonths(totalMonths float64) string {598 years, fracMonths := math.Modf(totalMonths / 12)599 months := int(math.Round(fracMonths * 12))600601 return fmt.Sprintf("%d years %d months", int(years), months)602}603604// FormatMonthsInt converts an int64 representing months into a "X years Y months" string.605func formatMonthsInt(totalMonths int64) string {606 // Use integer division to get the number of years607 years := totalMonths / 12608609 // Use the modulo operator to get the remaining months610 months := totalMonths % 12611612 return fmt.Sprintf("%d years %d months", years, months)613}614615func tryParseInt(s string, def int) int {616 i, err := strconv.Atoi(s)617 if err != nil {618 return def619 }620 return i621}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.
Generated by searchcode.com · powered by code_get_file + code_get_findings