Skip to content

Commit a0973af

Browse files
authored
feat(internal/conventionalcommits): support commit override and nested commits in parsing (#1776)
This change moves the parsing logic to a dedicated internal/conventionalcommits package and adds support for commit overrides and nested commits. - Commit Override: A BEGIN_COMMIT_OVERRIDE/END_COMMIT_OVERRIDE block can be used to replace the entire commit message with the content inside the block. - Nested Commits: BEGIN_NESTED_COMMIT/END_NESTED_COMMIT blocks allow for multiple conventional commits to be parsed from a single commit message. Note: an "isNested" field is added for requirement "nested commits information will only be used for release notes, and not for bumping semver", see #1769 refer to [go/librarian:release-please-lite](http://go/librarian:release-please-lite#heading=h.5bformyuaenc) for discussions on supported/unsupported edge case scenarios. For #1695
1 parent 7cb24f9 commit a0973af

File tree

5 files changed

+553
-197
lines changed

5 files changed

+553
-197
lines changed

internal/gitrepo/conventional_commits.go renamed to internal/conventionalcommits/conventional_commits.go

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package gitrepo
15+
package conventionalcommits
1616

1717
import (
1818
"fmt"
@@ -32,10 +32,12 @@ type ConventionalCommit struct {
3232
Description string
3333
// Body is the long-form description of the change.
3434
Body string
35-
// Footers contain metadata (e.g.,"BREAKING CHANGE", "Reviewed-by").
35+
// Footers contain metadata (e.g,"BREAKING CHANGE", "Reviewed-by").
3636
Footers map[string]string
3737
// IsBreaking indicates if the commit introduces a breaking change.
3838
IsBreaking bool
39+
// IsNested indicates if the commit is a nested commit.
40+
IsNested bool
3941
// SHA is the full commit hash.
4042
SHA string
4143
}
@@ -137,19 +139,111 @@ func parseFooters(footerLines []string) (footers map[string]string, isBreaking b
137139
return footers, isBreaking
138140
}
139141

140-
// ParseCommit parses a single commit message and returns a ConventionalCommit.
141-
// If the commit message does not follow the conventional commit format, it
142-
// logs a warning and returns a nil commit and no error.
143-
func ParseCommit(message, hashString string) (*ConventionalCommit, error) {
144-
trimmedMessage := strings.TrimSpace(message)
142+
const (
143+
beginCommitOverride = "BEGIN_COMMIT_OVERRIDE"
144+
endCommitOverride = "END_COMMIT_OVERRIDE"
145+
beginNestedCommit = "BEGIN_NESTED_COMMIT"
146+
endNestedCommit = "END_NESTED_COMMIT"
147+
)
148+
149+
func extractCommitMessageOverride(message string) string {
150+
beginIndex := strings.Index(message, beginCommitOverride)
151+
if beginIndex == -1 {
152+
return message
153+
}
154+
afterBegin := message[beginIndex+len(beginCommitOverride):]
155+
endIndex := strings.Index(afterBegin, endCommitOverride)
156+
if endIndex == -1 {
157+
return message
158+
}
159+
return strings.TrimSpace(afterBegin[:endIndex])
160+
}
161+
162+
// commitPart holds the raw string of a commit message and whether it's nested.
163+
type commitPart struct {
164+
message string
165+
isNested bool
166+
}
167+
168+
func extractCommitParts(message string) []commitPart {
169+
parts := strings.Split(message, beginNestedCommit)
170+
var commitParts []commitPart
171+
172+
// The first part is the primary commit.
173+
if len(parts) > 0 && strings.TrimSpace(parts[0]) != "" {
174+
commitParts = append(commitParts, commitPart{
175+
message: strings.TrimSpace(parts[0]),
176+
isNested: false,
177+
})
178+
}
179+
180+
// The rest of the parts are nested commits.
181+
for i := 1; i < len(parts); i++ {
182+
nestedPart := parts[i]
183+
endIndex := strings.Index(nestedPart, endNestedCommit)
184+
if endIndex == -1 {
185+
// Malformed, ignore.
186+
continue
187+
}
188+
commitStr := strings.TrimSpace(nestedPart[:endIndex])
189+
if commitStr == "" {
190+
continue
191+
}
192+
commitParts = append(commitParts, commitPart{
193+
message: commitStr,
194+
isNested: true,
195+
})
196+
}
197+
return commitParts
198+
}
199+
200+
// ParseCommits parses a commit message into a slice of ConventionalCommit structs.
201+
//
202+
// It supports an override block wrapped in BEGIN_COMMIT_OVERRIDE and
203+
// END_COMMIT_OVERRIDE. If found, this block takes precedence, and only its
204+
// content will be parsed.
205+
//
206+
// The message can also contain multiple nested commits, each wrapped in
207+
// BEGIN_NESTED_COMMIT and END_NESTED_COMMIT markers.
208+
//
209+
// Malformed override or nested blocks (e.g., with a missing end marker) are
210+
// ignored. Any commit part that is found but fails to parse as a valid
211+
// conventional commit is logged and skipped.
212+
func ParseCommits(message, hashString string) ([]*ConventionalCommit, error) {
213+
if strings.TrimSpace(message) == "" {
214+
return nil, fmt.Errorf("empty commit message")
215+
}
216+
message = extractCommitMessageOverride(message)
217+
218+
var commits []*ConventionalCommit
219+
220+
for _, part := range extractCommitParts(message) {
221+
c, err := parseSimpleCommit(part, hashString)
222+
if err != nil {
223+
slog.Warn("failed to parse commit part", "commit", part.message, "error", err)
224+
continue
225+
}
226+
227+
if c != nil {
228+
commits = append(commits, c)
229+
}
230+
}
231+
232+
return commits, nil
233+
}
234+
235+
// parseSimpleCommit parses a simple commit message and returns a ConventionalCommit.
236+
// A simple commit message is commit that does not include override or nested commits.
237+
func parseSimpleCommit(commitPart commitPart, hashString string) (*ConventionalCommit, error) {
238+
trimmedMessage := strings.TrimSpace(commitPart.message)
145239
if trimmedMessage == "" {
146240
return nil, fmt.Errorf("empty commit message")
147241
}
148242
lines := strings.Split(trimmedMessage, "\n")
149243

150244
header, ok := parseHeader(lines[0])
151245
if !ok {
152-
slog.Warn("Invalid conventional commit message", "message", message, "hash", hashString)
246+
slog.Warn("Invalid conventional commit message", "message", commitPart.message, "hash", hashString)
153247
return nil, nil
154248
}
155249

@@ -164,6 +258,7 @@ func ParseCommit(message, hashString string) (*ConventionalCommit, error) {
164258
Body: strings.TrimSpace(strings.Join(bodyLines, "\n")),
165259
Footers: footers,
166260
IsBreaking: header.IsBreaking || footerIsBreaking,
261+
IsNested: commitPart.isNested,
167262
SHA: hashString,
168263
}, nil
169264
}

0 commit comments

Comments
 (0)