Skip to content

Commit b3ca16f

Browse files
authored
Merge pull request #84 from thomas-vilte/dev
Refactor: Enhance Git and Changelog Operations Robustness
2 parents a8f87f4 + f4165a5 commit b3ca16f

2 files changed

Lines changed: 118 additions & 16 deletions

File tree

internal/git/git_service.go

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package git
33
import (
44
"context"
55
"fmt"
6+
"os"
67
"os/exec"
78
"regexp"
89
"sort"
@@ -31,11 +32,32 @@ func (s *GitService) SetFallback(name, email string) {
3132

3233
// HasStagedChanges checks if there are changes in the staging area
3334
func (s *GitService) HasStagedChanges(ctx context.Context) bool {
35+
log := logger.FromContext(ctx)
36+
37+
repoRoot, err := s.getRepoRoot(ctx)
38+
if err != nil {
39+
log.Error("failed to get repo root", "error", err)
40+
return false
41+
}
42+
3443
cmd := exec.CommandContext(ctx, "git", "diff", "--cached", "--quiet")
35-
err := cmd.Run()
44+
cmd.Dir = repoRoot
45+
err = cmd.Run()
3646

37-
// If the command returns an error (exit status 1), it means there are staged changes
38-
return err != nil && cmd.ProcessState.ExitCode() == 1
47+
hasChanges := err != nil && cmd.ProcessState.ExitCode() == 1
48+
49+
statusCmd := exec.CommandContext(ctx, "git", "status", "--porcelain")
50+
statusCmd.Dir = repoRoot
51+
statusOutput, _ := statusCmd.Output()
52+
statusStr := strings.TrimSpace(string(statusOutput))
53+
54+
log.Debug("checking staged changes",
55+
"repo_root", repoRoot,
56+
"has_staged_changes", hasChanges,
57+
"exit_code", cmd.ProcessState.ExitCode(),
58+
"git_status", statusStr)
59+
60+
return hasChanges
3961
}
4062

4163
func (s *GitService) GetChangedFiles(ctx context.Context) ([]string, error) {
@@ -109,7 +131,6 @@ func (s *GitService) GetDiff(ctx context.Context) (string, error) {
109131
}
110132
}
111133

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

156+
repoRoot, err := s.getRepoRoot(ctx)
157+
if err != nil {
158+
return err
159+
}
160+
135161
log.Debug("creating git commit",
136162
"message_length", len(message))
137163

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

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

169196
func (s *GitService) AddFileToStaging(ctx context.Context, file string) error {
197+
log := logger.FromContext(ctx)
198+
170199
repoRoot, err := s.getRepoRoot(ctx)
171200
if err != nil {
172201
return err
173202
}
174203

175-
cmd := exec.CommandContext(ctx, "git", "add", "-A", "--", file)
204+
log.Debug("adding file to staging",
205+
"file", file,
206+
"repo_root", repoRoot)
207+
208+
fullPath := repoRoot + "/" + file
209+
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
210+
log.Error("file does not exist",
211+
"file", file,
212+
"full_path", fullPath)
213+
return errors.ErrAddFile.WithError(err).WithContext("file", file).WithContext("reason", "file_not_found")
214+
}
215+
216+
cmd := exec.CommandContext(ctx, "git", "add", "--", file)
176217
cmd.Dir = repoRoot
177218
var stderr strings.Builder
178219
cmd.Stderr = &stderr
179220

180221
if err := cmd.Run(); err != nil {
181222
stderrStr := strings.TrimSpace(stderr.String())
223+
log.Error("failed to add file to staging",
224+
"file", file,
225+
"error", err,
226+
"stderr", stderrStr)
182227
return errors.ErrAddFile.WithError(err).WithContext("file", file).WithContext("stderr", stderrStr)
183228
}
229+
230+
statusCmd := exec.CommandContext(ctx, "git", "status", "--porcelain", "--", file)
231+
statusCmd.Dir = repoRoot
232+
statusOutput, _ := statusCmd.Output()
233+
log.Debug("git status after add",
234+
"file", file,
235+
"status", strings.TrimSpace(string(statusOutput)))
236+
237+
log.Debug("file added to staging successfully",
238+
"file", file)
184239
return nil
185240
}
186241

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

278333
var args []string
279334
if tag == "" {
280-
// if no previous tag exists, get all commits
281335
args = []string{"log", "--pretty=format:%H|%an|%ae|%ad|%s|%b", "--no-merges", "--date=iso"}
282336
} else {
283337
if err := s.ValidateTagExists(ctx, tag); err != nil {

internal/services/release_service.go

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,10 @@ func (s *ReleaseService) UpdateLocalChangelog(release *models.Release, notes *mo
362362
log.Warn("failed to move Unreleased section", "error", err)
363363
} else {
364364
log.Info("moved Unreleased section to new version", "version", release.Version)
365-
return nil
365+
if s.hasUnreleasedContent(content) {
366+
return nil
367+
}
368+
log.Debug("unreleased section was empty, will add new version entry")
366369
}
367370
}
368371
}
@@ -464,11 +467,12 @@ func (s *ReleaseService) prependToChangelog(filename, newContent string) error {
464467
func (s *ReleaseService) prependToChangelogLegacy(filename, current, newContent string) error {
465468
var sb strings.Builder
466469

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

469-
if idx != -1 {
470-
pre := current[:idx]
471-
post := current[idx:]
473+
if loc != nil {
474+
pre := current[:loc[0]]
475+
post := current[loc[0]:]
472476

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

491495
result := sb.String()
492496

497+
// Remove empty Unreleased sections that might be between versions
498+
result = s.removeEmptyUnreleasedSections(result)
499+
493500
result = s.consolidateLinkDefinitions(result)
494501

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

505+
// removeEmptyUnreleasedSections removes empty ## [Unreleased] sections that appear between versions
506+
func (s *ReleaseService) removeEmptyUnreleasedSections(content string) string {
507+
emptyUnreleasedPattern := regexp.MustCompile(`(?s)## \[Unreleased]\s*\n(?=## \[)`)
508+
return emptyUnreleasedPattern.ReplaceAllString(content, "")
509+
}
510+
498511
// consolidateLinkDefinitions removes duplicate link reference definitions
499512
func (s *ReleaseService) consolidateLinkDefinitions(content string) string {
500513
linkDefPattern := regexp.MustCompile(`(?m)^\[([^]]+)]:\s*(.+)$`)
@@ -560,12 +573,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
560573
return nil
561574
}
562575

563-
idx := strings.Index(current, "\n## ")
564-
if idx == -1 {
576+
versionPattern := regexp.MustCompile(`\n## \[[^]]+]`)
577+
loc := versionPattern.FindStringIndex(current)
578+
579+
if loc == nil {
565580
current = strings.TrimSpace(current) + "\n\n## [Unreleased]\n\n"
566581
} else {
567-
pre := current[:idx]
568-
post := current[idx:]
582+
pre := current[:loc[0]]
583+
post := current[loc[0]:]
569584
current = strings.TrimSpace(pre) + "\n\n## [Unreleased]\n\n" + post
570585
}
571586

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

602+
// hasUnreleasedContent checks if the Unreleased section has actual content
603+
func (s *ReleaseService) hasUnreleasedContent(content []byte) bool {
604+
return s.parseUnreleasedSection(string(content)) != ""
605+
}
606+
587607
// MoveUnreleasedToVersion moves Unreleased section content to a new version
588608
func (s *ReleaseService) MoveUnreleasedToVersion(filename string, release *models.Release, notes *models.ReleaseNotes) error {
589609
content, err := os.ReadFile(filename)
@@ -601,11 +621,17 @@ func (s *ReleaseService) MoveUnreleasedToVersion(filename string, release *model
601621

602622
unreleasedContent := s.parseUnreleasedSection(current)
603623

624+
log := logger.FromContext(context.Background())
625+
log.Debug("parsed unreleased section",
626+
"unreleased_content", unreleasedContent,
627+
"unreleased_content_length", len(unreleasedContent),
628+
"is_empty", unreleasedContent == "")
629+
604630
if unreleasedContent == "" {
631+
log.Info("unreleased section is empty, skipping migration")
605632
return nil
606633
}
607634

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

646+
log.Debug("writing modified changelog without unreleased section",
647+
"filename", filename,
648+
"content_length", len(current))
649+
620650
if err := os.WriteFile(filename, []byte(current), 0644); err != nil {
621651
return domainErrors.NewAppError(
622652
domainErrors.TypeInternal,
@@ -628,6 +658,8 @@ func (s *ReleaseService) MoveUnreleasedToVersion(filename string, release *model
628658
)
629659
}
630660

661+
log.Debug("changelog written successfully, prepending new version")
662+
631663
if err := s.prependToChangelog(filename, versionEntry); err != nil {
632664
return domainErrors.NewAppError(
633665
domainErrors.TypeInternal,
@@ -974,9 +1006,14 @@ func (s *ReleaseService) formatReleaseItem(item models.ReleaseItem) string {
9741006
}
9751007

9761008
func (s *ReleaseService) CommitChangelog(ctx context.Context, version string) error {
1009+
log := logger.FromContext(ctx)
1010+
log.Info("starting changelog commit process", "version", version)
1011+
9771012
if err := s.git.AddFileToStaging(ctx, "CHANGELOG.md"); err != nil {
1013+
log.Error("failed to add CHANGELOG.md to staging", "error", err)
9781014
return domainErrors.NewAppError(domainErrors.TypeGit, "failed to add CHANGELOG.md to staging", err)
9791015
}
1016+
log.Debug("CHANGELOG.md added to staging")
9801017

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

9901027
if _, err := os.Stat(versionFile); err == nil {
1028+
log.Debug("adding version file to staging", "file", versionFile)
9911029
if err := s.git.AddFileToStaging(ctx, versionFile); err != nil {
1030+
log.Error("failed to add version file to staging", "file", versionFile, "error", err)
9921031
return domainErrors.NewAppError(domainErrors.TypeGit, fmt.Sprintf("failed to add version file to staging: %s", versionFile), err)
9931032
}
1033+
log.Debug("version file added to staging", "file", versionFile)
1034+
} else {
1035+
log.Debug("version file not found, skipping", "file", versionFile)
9941036
}
9951037

1038+
log.Debug("checking for staged changes")
9961039
if !s.git.HasStagedChanges(ctx) {
1040+
log.Error("no staged changes detected after adding files")
9971041
return domainErrors.ErrNoChanges
9981042
}
1043+
log.Debug("staged changes detected, proceeding with commit")
9991044

10001045
message := fmt.Sprintf("chore: update changelog and bump version to %s", version)
10011046
if err := s.git.CreateCommit(ctx, message); err != nil {
1047+
log.Error("failed to create commit", "error", err)
10021048
return domainErrors.NewAppError(domainErrors.TypeGit, "failed to commit changelog and version bump", err)
10031049
}
1050+
1051+
log.Info("changelog commit process completed successfully")
10041052
return nil
10051053

10061054
}

0 commit comments

Comments
 (0)