Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 60 additions & 6 deletions internal/git/git_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package git
import (
"context"
"fmt"
"os"
"os/exec"
"regexp"
"sort"
Expand Down Expand Up @@ -31,11 +32,32 @@ func (s *GitService) SetFallback(name, email string) {

// HasStagedChanges checks if there are changes in the staging area
func (s *GitService) HasStagedChanges(ctx context.Context) bool {
log := logger.FromContext(ctx)

repoRoot, err := s.getRepoRoot(ctx)
if err != nil {
log.Error("failed to get repo root", "error", err)
return false
}

cmd := exec.CommandContext(ctx, "git", "diff", "--cached", "--quiet")
err := cmd.Run()
cmd.Dir = repoRoot
err = cmd.Run()

// If the command returns an error (exit status 1), it means there are staged changes
return err != nil && cmd.ProcessState.ExitCode() == 1
hasChanges := err != nil && cmd.ProcessState.ExitCode() == 1

statusCmd := exec.CommandContext(ctx, "git", "status", "--porcelain")
statusCmd.Dir = repoRoot
statusOutput, _ := statusCmd.Output()
statusStr := strings.TrimSpace(string(statusOutput))

log.Debug("checking staged changes",
"repo_root", repoRoot,
"has_staged_changes", hasChanges,
"exit_code", cmd.ProcessState.ExitCode(),
"git_status", statusStr)

return hasChanges
}

func (s *GitService) GetChangedFiles(ctx context.Context) ([]string, error) {
Expand Down Expand Up @@ -109,7 +131,6 @@ func (s *GitService) GetDiff(ctx context.Context) (string, error) {
}
}

// If still no diff after checking untracked files
if combinedDiff == "" {
log.Warn("no differences detected in repository")
return "", errors.ErrNoDiff
Expand All @@ -132,10 +153,16 @@ func (s *GitService) CreateCommit(ctx context.Context, message string) error {
return errors.ErrNoChanges
}

repoRoot, err := s.getRepoRoot(ctx)
if err != nil {
return err
}

log.Debug("creating git commit",
"message_length", len(message))

cmd := exec.CommandContext(ctx, "git", "commit", "-m", message)
cmd.Dir = repoRoot
var stderr strings.Builder
cmd.Stderr = &stderr

Expand Down Expand Up @@ -167,20 +194,48 @@ func (s *GitService) CreateCommit(ctx context.Context, message string) error {
}

func (s *GitService) AddFileToStaging(ctx context.Context, file string) error {
log := logger.FromContext(ctx)

repoRoot, err := s.getRepoRoot(ctx)
if err != nil {
return err
}

cmd := exec.CommandContext(ctx, "git", "add", "-A", "--", file)
log.Debug("adding file to staging",
"file", file,
"repo_root", repoRoot)

fullPath := repoRoot + "/" + file
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
log.Error("file does not exist",
"file", file,
"full_path", fullPath)
return errors.ErrAddFile.WithError(err).WithContext("file", file).WithContext("reason", "file_not_found")
}

cmd := exec.CommandContext(ctx, "git", "add", "--", file)
cmd.Dir = repoRoot
var stderr strings.Builder
cmd.Stderr = &stderr

if err := cmd.Run(); err != nil {
stderrStr := strings.TrimSpace(stderr.String())
log.Error("failed to add file to staging",
"file", file,
"error", err,
"stderr", stderrStr)
return errors.ErrAddFile.WithError(err).WithContext("file", file).WithContext("stderr", stderrStr)
}

statusCmd := exec.CommandContext(ctx, "git", "status", "--porcelain", "--", file)
statusCmd.Dir = repoRoot
statusOutput, _ := statusCmd.Output()
log.Debug("git status after add",
"file", file,
"status", strings.TrimSpace(string(statusOutput)))

log.Debug("file added to staging successfully",
"file", file)
return nil
}

Expand Down Expand Up @@ -277,7 +332,6 @@ func (s *GitService) GetCommitsSinceTag(ctx context.Context, tag string) ([]mode

var args []string
if tag == "" {
// if no previous tag exists, get all commits
args = []string{"log", "--pretty=format:%H|%an|%ae|%ad|%s|%b", "--no-merges", "--date=iso"}
} else {
if err := s.ValidateTagExists(ctx, tag); err != nil {
Expand Down
68 changes: 58 additions & 10 deletions internal/services/release_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,10 @@ func (s *ReleaseService) UpdateLocalChangelog(release *models.Release, notes *mo
log.Warn("failed to move Unreleased section", "error", err)
} else {
log.Info("moved Unreleased section to new version", "version", release.Version)
return nil
if s.hasUnreleasedContent(content) {
return nil
}
log.Debug("unreleased section was empty, will add new version entry")
}
}
}
Expand Down Expand Up @@ -464,11 +467,12 @@ func (s *ReleaseService) prependToChangelog(filename, newContent string) error {
func (s *ReleaseService) prependToChangelogLegacy(filename, current, newContent string) error {
var sb strings.Builder

idx := strings.Index(current, "\n## ")
versionPattern := regexp.MustCompile(`\n## \[[^]]+]`)
loc := versionPattern.FindStringIndex(current)

if idx != -1 {
pre := current[:idx]
post := current[idx:]
if loc != nil {
pre := current[:loc[0]]
post := current[loc[0]:]

sb.WriteString(strings.TrimSpace(pre))
sb.WriteString("\n\n")
Expand All @@ -490,11 +494,20 @@ func (s *ReleaseService) prependToChangelogLegacy(filename, current, newContent

result := sb.String()

// Remove empty Unreleased sections that might be between versions
result = s.removeEmptyUnreleasedSections(result)

result = s.consolidateLinkDefinitions(result)

return os.WriteFile(filename, []byte(result), 0644)
}

// removeEmptyUnreleasedSections removes empty ## [Unreleased] sections that appear between versions
func (s *ReleaseService) removeEmptyUnreleasedSections(content string) string {
emptyUnreleasedPattern := regexp.MustCompile(`(?s)## \[Unreleased]\s*\n(?=## \[)`)
return emptyUnreleasedPattern.ReplaceAllString(content, "")
}

// consolidateLinkDefinitions removes duplicate link reference definitions
func (s *ReleaseService) consolidateLinkDefinitions(content string) string {
linkDefPattern := regexp.MustCompile(`(?m)^\[([^]]+)]:\s*(.+)$`)
Expand Down Expand Up @@ -560,12 +573,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
return nil
}

idx := strings.Index(current, "\n## ")
if idx == -1 {
versionPattern := regexp.MustCompile(`\n## \[[^]]+]`)
loc := versionPattern.FindStringIndex(current)

if loc == nil {
current = strings.TrimSpace(current) + "\n\n## [Unreleased]\n\n"
} else {
pre := current[:idx]
post := current[idx:]
pre := current[:loc[0]]
post := current[loc[0]:]
current = strings.TrimSpace(pre) + "\n\n## [Unreleased]\n\n" + post
}

Expand All @@ -584,6 +599,11 @@ func (s *ReleaseService) parseUnreleasedSection(content string) string {
return strings.TrimSpace(matches[1])
}

// hasUnreleasedContent checks if the Unreleased section has actual content
func (s *ReleaseService) hasUnreleasedContent(content []byte) bool {
return s.parseUnreleasedSection(string(content)) != ""
}

// MoveUnreleasedToVersion moves Unreleased section content to a new version
func (s *ReleaseService) MoveUnreleasedToVersion(filename string, release *models.Release, notes *models.ReleaseNotes) error {
content, err := os.ReadFile(filename)
Expand All @@ -601,11 +621,17 @@ func (s *ReleaseService) MoveUnreleasedToVersion(filename string, release *model

unreleasedContent := s.parseUnreleasedSection(current)

log := logger.FromContext(context.Background())
log.Debug("parsed unreleased section",
"unreleased_content", unreleasedContent,
"unreleased_content_length", len(unreleasedContent),
"is_empty", unreleasedContent == "")

if unreleasedContent == "" {
log.Info("unreleased section is empty, skipping migration")
return nil
}

log := logger.FromContext(context.Background())
log.Info("moving Unreleased section to new version",
"version", release.Version,
"unreleased_content_length", len(unreleasedContent))
Expand All @@ -617,6 +643,10 @@ func (s *ReleaseService) MoveUnreleasedToVersion(filename string, release *model
unreleasedPattern := regexp.MustCompile(`(?s)## \[Unreleased]\s*\n.*?(\n## \[|$)`)
current = unreleasedPattern.ReplaceAllString(current, "$1")

log.Debug("writing modified changelog without unreleased section",
"filename", filename,
"content_length", len(current))

if err := os.WriteFile(filename, []byte(current), 0644); err != nil {
return domainErrors.NewAppError(
domainErrors.TypeInternal,
Expand All @@ -628,6 +658,8 @@ func (s *ReleaseService) MoveUnreleasedToVersion(filename string, release *model
)
}

log.Debug("changelog written successfully, prepending new version")

if err := s.prependToChangelog(filename, versionEntry); err != nil {
return domainErrors.NewAppError(
domainErrors.TypeInternal,
Expand Down Expand Up @@ -974,9 +1006,14 @@ func (s *ReleaseService) formatReleaseItem(item models.ReleaseItem) string {
}

func (s *ReleaseService) CommitChangelog(ctx context.Context, version string) error {
log := logger.FromContext(ctx)
log.Info("starting changelog commit process", "version", version)

if err := s.git.AddFileToStaging(ctx, "CHANGELOG.md"); err != nil {
log.Error("failed to add CHANGELOG.md to staging", "error", err)
return domainErrors.NewAppError(domainErrors.TypeGit, "failed to add CHANGELOG.md to staging", err)
}
log.Debug("CHANGELOG.md added to staging")

versionFile, _, err := s.FindVersionFile(ctx)
if err != nil {
Expand All @@ -988,19 +1025,30 @@ func (s *ReleaseService) CommitChangelog(ctx context.Context, version string) er
}

if _, err := os.Stat(versionFile); err == nil {
log.Debug("adding version file to staging", "file", versionFile)
if err := s.git.AddFileToStaging(ctx, versionFile); err != nil {
log.Error("failed to add version file to staging", "file", versionFile, "error", err)
return domainErrors.NewAppError(domainErrors.TypeGit, fmt.Sprintf("failed to add version file to staging: %s", versionFile), err)
}
log.Debug("version file added to staging", "file", versionFile)
} else {
log.Debug("version file not found, skipping", "file", versionFile)
}

log.Debug("checking for staged changes")
if !s.git.HasStagedChanges(ctx) {
log.Error("no staged changes detected after adding files")
return domainErrors.ErrNoChanges
}
log.Debug("staged changes detected, proceeding with commit")

message := fmt.Sprintf("chore: update changelog and bump version to %s", version)
if err := s.git.CreateCommit(ctx, message); err != nil {
log.Error("failed to create commit", "error", err)
return domainErrors.NewAppError(domainErrors.TypeGit, "failed to commit changelog and version bump", err)
}

log.Info("changelog commit process completed successfully")
return nil

}
Expand Down
Loading