< Summary - go-semantic-release Coverage

Line coverage
80%
Covered lines: 84
Uncovered lines: 21
Coverable lines: 105
Total lines: 212
Line coverage: 80%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
run0%00100%
CurrentBranch0%00100%
ListTags0%0065.22%
CommitsSince0%0053.85%
FilesChangedInCommit0%0060%
CreateTag0%0093.75%
PushTag0%00100%
HeadHash0%00100%
RemoteURL0%00100%
Stage0%00100%
Commit0%00100%
Push0%00100%

File(s)

/home/runner/work/go-semantic-release/go-semantic-release/internal/adapters/git/repository.go

#LineLine coverage
 1package git
 2
 3import (
 4  "bytes"
 5  "context"
 6  "fmt"
 7  "os/exec"
 8  "strings"
 9  "time"
 10
 11  "github.com/jedi-knights/go-semantic-release/internal/domain"
 12  "github.com/jedi-knights/go-semantic-release/internal/ports"
 13)
 14
 15// Compile-time interface compliance check.
 16var _ ports.GitRepository = (*Repository)(nil)
 17
 18// Repository implements ports.GitRepository using the git CLI.
 19type Repository struct {
 20  workDir string
 21}
 22
 23// NewRepository creates a new git CLI adapter.
 24func NewRepository(workDir string) *Repository {
 25  return &Repository{workDir: workDir}
 26}
 27
 1828func (r *Repository) run(ctx context.Context, args ...string) (string, error) {
 1829  cmd := exec.CommandContext(ctx, "git", args...)
 1830  cmd.Dir = r.workDir
 1831  var stdout, stderr bytes.Buffer
 1832  cmd.Stdout = &stdout
 1833  cmd.Stderr = &stderr
 434  if err := cmd.Run(); err != nil {
 435    return "", fmt.Errorf("git %s: %s: %w", strings.Join(args, " "), stderr.String(), err)
 436  }
 1437  return strings.TrimSpace(stdout.String()), nil
 38}
 39
 140func (r *Repository) CurrentBranch(ctx context.Context) (string, error) {
 141  return r.run(ctx, "rev-parse", "--abbrev-ref", "HEAD")
 142}
 43
 244func (r *Repository) ListTags(ctx context.Context) ([]domain.Tag, error) {
 245  output, err := r.run(ctx, "tag", "--list", "--sort=-version:refname")
 046  if err != nil {
 047    return nil, err
 048  }
 149  if output == "" {
 150    return nil, nil
 151  }
 52
 153  lines := strings.Split(output, "\n")
 154  tags := make([]domain.Tag, 0, len(lines))
 155  for _, line := range lines {
 156    line = strings.TrimSpace(line)
 057    if line == "" {
 058      continue
 59    }
 160    hash, err := r.run(ctx, "rev-list", "-1", line)
 061    if err != nil {
 062      return nil, fmt.Errorf("resolving tag %s: %w", line, err)
 063    }
 164    tags = append(tags, domain.Tag{
 165      Name: line,
 166      Hash: hash,
 167    })
 68  }
 169  return tags, nil
 70}
 71
 272func (r *Repository) CommitsSince(ctx context.Context, sinceHash string) ([]domain.Commit, error) {
 273  args := []string{"log", "--format=%H|%an|%ae|%aI|%s|%b%x00"}
 174  if sinceHash != "" {
 175    args = append(args, sinceHash+"..HEAD")
 176  }
 77
 278  output, err := r.run(ctx, args...)
 079  if err != nil {
 080    return nil, err
 081  }
 082  if output == "" {
 083    return nil, nil
 084  }
 85
 286  return parseCommitLog(output)
 87}
 88
 89func parseCommitLog(output string) ([]domain.Commit, error) {
 90  entries := strings.Split(output, "\x00")
 91  commits := make([]domain.Commit, 0, len(entries))
 92
 93  for _, entry := range entries {
 94    entry = strings.TrimSpace(entry)
 95    if entry == "" {
 96      continue
 97    }
 98
 99    commit, err := parseCommitEntry(entry)
 100    if err != nil {
 101      continue // skip unparseable entries
 102    }
 103    commits = append(commits, commit)
 104  }
 105  return commits, nil
 106}
 107
 108func parseCommitEntry(entry string) (domain.Commit, error) {
 109  // First line: hash|author|email|date|subject
 110  // Remaining: body
 111  lines := strings.SplitN(entry, "\n", 2)
 112  firstLine := lines[0]
 113
 114  parts := strings.SplitN(firstLine, "|", 6)
 115  if len(parts) < 5 {
 116    return domain.Commit{}, fmt.Errorf("unexpected commit format: %q", firstLine)
 117  }
 118
 119  date, err := time.Parse(time.RFC3339, parts[3])
 120  if err != nil {
 121    return domain.Commit{}, fmt.Errorf("parsing commit date %q: %w", parts[3], err)
 122  }
 123
 124  body := ""
 125  if len(parts) >= 6 {
 126    body = parts[5]
 127  }
 128  if len(lines) > 1 {
 129    body = body + "\n" + lines[1]
 130  }
 131
 132  return domain.Commit{
 133    Hash:        parts[0],
 134    Author:      parts[1],
 135    AuthorEmail: parts[2],
 136    Date:        date,
 137    Message:     parts[4],
 138    Body:        strings.TrimSpace(body),
 139  }, nil
 140}
 141
 1142func (r *Repository) FilesChangedInCommit(ctx context.Context, hash string) ([]string, error) {
 1143  output, err := r.run(ctx, "diff-tree", "--no-commit-id", "--name-only", "-r", hash)
 0144  if err != nil {
 0145    return nil, err
 0146  }
 0147  if output == "" {
 0148    return nil, nil
 0149  }
 1150  lines := strings.Split(output, "\n")
 1151  result := make([]string, 0, len(lines))
 1152  for _, l := range lines {
 1153    if l != "" {
 1154      result = append(result, l)
 1155    }
 156  }
 1157  return result, nil
 158}
 159
 4160func (r *Repository) CreateTag(ctx context.Context, name, hash, message string) error {
 4161  var err error
 1162  if message != "" {
 1163    _, err = r.run(ctx, "tag", "-a", name, hash, "-m", message)
 1164  } else {
 3165    _, err = r.run(ctx, "tag", name, hash)
 3166  }
 3167  if err == nil {
 3168    return nil
 3169  }
 170  // When the tag already exists, check whether it resolves to the same commit.
 171  // If it does, the operation is idempotent — return ErrTagAlreadyExists so
 172  // the caller can handle the re-run case without treating it as a hard failure.
 1173  if strings.Contains(err.Error(), "already exists") {
 1174    existing, resolveErr := r.run(ctx, "rev-parse", name+"^{commit}")
 1175    if resolveErr == nil && existing == hash {
 1176      return domain.ErrTagAlreadyExists
 1177    }
 178  }
 0179  return err
 180}
 181
 1182func (r *Repository) PushTag(ctx context.Context, name string) error {
 1183  _, err := r.run(ctx, "push", "origin", name)
 1184  return err
 1185}
 186
 1187func (r *Repository) HeadHash(ctx context.Context) (string, error) {
 1188  return r.run(ctx, "rev-parse", "HEAD")
 1189}
 190
 1191func (r *Repository) RemoteURL(ctx context.Context) (string, error) {
 1192  return r.run(ctx, "remote", "get-url", "origin")
 1193}
 194
 2195func (r *Repository) Stage(ctx context.Context, files []string) error {
 1196  if len(files) == 0 {
 1197    return nil
 1198  }
 1199  args := append([]string{"add", "--"}, files...)
 1200  _, err := r.run(ctx, args...)
 1201  return err
 202}
 203
 1204func (r *Repository) Commit(ctx context.Context, message string) error {
 1205  _, err := r.run(ctx, "commit", "-m", message)
 1206  return err
 1207}
 208
 1209func (r *Repository) Push(ctx context.Context) error {
 1210  _, err := r.run(ctx, "push", "origin", "HEAD")
 1211  return err
 1212}