forgejo/modules/git/repo_stats.go
Mathieu Fenniak f93d2cb261 ci: detect and prevent empty case statements in Go code (#11593)
One of the security patches released 2026-03-09 [fixed a vulnerability](d1c7b04d09) caused by a misapplication of Go `case` statements, where the implementation would have been correct if Go `case` statements automatically fall through to the next case block, but they do not.  This PR adds a semgrep rule which detects any empty `case` statement and raises an error, in order to prevent this coding mistake in the future.

For example, code like this will now trigger a build error:
```go
	switch setting.Protocol {
	case setting.HTTPUnix:
	case setting.FCGI:
	case setting.FCGIUnix:
	default:
		defaultLocalURL := string(setting.Protocol) + "://"
	}
```

Example error:
```
    cmd/web.go
   ❯❯❱ semgrep.config.forgejo-switch-empty-case
          switch has a case block with no content. This is treated as "break" by Go, but developers may
          confuse it for "fallthrough".  To fix this error, disambiguate by using "break" or
          "fallthrough".

          279┆ switch setting.Protocol {
          280┆ case setting.HTTPUnix:
          281┆ case setting.FCGI:
          282┆ case setting.FCGIUnix:
          283┆ default:
          284┆   defaultLocalURL := string(setting.Protocol) + "://"
          285┆   if setting.HTTPAddr == "0.0.0.0" {
          286┆           defaultLocalURL += "localhost"
          287┆   } else {
          288┆           defaultLocalURL += setting.HTTPAddr
```

As described in the error output, this error can be fixed by explicitly listing `break` (the real Go behaviour, to do nothing in the block), or by listing `fallthrough` (if the intent was to fall through).

All existing code triggering this detection has been changed to `break` (or, rarely, irrelevant cases have been removed), which should maintain the same code functionality.  While performing this fixup, a light analysis was performed on each case and they *appeared* correct, but with ~65 cases I haven't gone into extreme depth.

Tests are present for the semgrep rule in `.semgrep/tests/go.go`.

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.
- [x] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11593
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
2026-03-10 02:50:28 +01:00

152 lines
3.9 KiB
Go

// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"bufio"
"context"
"fmt"
"os"
"sort"
"strconv"
"strings"
"time"
"forgejo.org/modules/container"
)
// CodeActivityStats represents git statistics data
type CodeActivityStats struct {
AuthorCount int64
CommitCount int64
ChangedFiles int64
Additions int64
Deletions int64
CommitCountInAllBranches int64
Authors []*CodeActivityAuthor
}
// CodeActivityAuthor represents git statistics data for commit authors
type CodeActivityAuthor struct {
Name string
Email string
Commits int64
}
// GetCodeActivityStats returns code statistics for activity page
func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) (*CodeActivityStats, error) {
stats := &CodeActivityStats{}
since := fromTime.Format(time.RFC3339)
stdout, _, runErr := NewCommand(repo.Ctx, "rev-list", "--count", "--no-merges", "--branches=*", "--date=iso").AddOptionFormat("--since='%s'", since).RunStdString(&RunOpts{Dir: repo.Path})
if runErr != nil {
return nil, runErr
}
c, err := strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
if err != nil {
return nil, err
}
stats.CommitCountInAllBranches = c
stdoutReader, stdoutWriter, err := os.Pipe()
if err != nil {
return nil, err
}
defer func() {
_ = stdoutReader.Close()
_ = stdoutWriter.Close()
}()
gitCmd := NewCommand(repo.Ctx, "log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso").AddOptionFormat("--since='%s'", since)
if len(branch) == 0 {
gitCmd.AddArguments("--branches=*")
} else {
gitCmd.AddArguments("--first-parent").AddDynamicArguments(branch)
}
stderr := new(strings.Builder)
err = gitCmd.Run(&RunOpts{
Env: []string{},
Dir: repo.Path,
Stdout: stdoutWriter,
Stderr: stderr,
PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
_ = stdoutWriter.Close()
scanner := bufio.NewScanner(stdoutReader)
scanner.Split(bufio.ScanLines)
stats.CommitCount = 0
stats.Additions = 0
stats.Deletions = 0
authors := make(map[string]*CodeActivityAuthor)
files := make(container.Set[string])
var author string
p := 0
for scanner.Scan() {
l := strings.TrimSpace(scanner.Text())
if l == "---" {
p = 1
} else if p == 0 {
continue
} else {
p++
}
if p > 4 && len(l) == 0 {
continue
}
switch p {
case 1: // Separator
break
case 2: // Commit sha-1
stats.CommitCount++
case 3: // Author
author = l
case 4: // E-mail
email := strings.ToLower(l)
if _, ok := authors[email]; !ok {
authors[email] = &CodeActivityAuthor{Name: author, Email: email, Commits: 0}
}
authors[email].Commits++
default: // Changed file
if parts := strings.Fields(l); len(parts) >= 3 {
if parts[0] != "-" {
if c, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64); err == nil {
stats.Additions += c
}
}
if parts[1] != "-" {
if c, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil {
stats.Deletions += c
}
}
files.Add(parts[2])
}
}
}
if err = scanner.Err(); err != nil {
_ = stdoutReader.Close()
return fmt.Errorf("GetCodeActivityStats scan: %w", err)
}
a := make([]*CodeActivityAuthor, 0, len(authors))
for _, v := range authors {
a = append(a, v)
}
// Sort authors descending depending on commit count
sort.Slice(a, func(i, j int) bool {
return a[i].Commits > a[j].Commits
})
stats.AuthorCount = int64(len(authors))
stats.ChangedFiles = int64(len(files))
stats.Authors = a
_ = stdoutReader.Close()
return nil
},
})
if err != nil {
return nil, fmt.Errorf("Failed to get GetCodeActivityStats for repository.\nError: %w\nStderr: %s", err, stderr)
}
return stats, nil
}