Skip to content

Commit b2e2c93

Browse files
authored
feat(librarian): parse conventional commits for library since last released version -part2 (#1765)
Adds method to parse commits since last released for a library to slices of ConventionalCommit. Also adds a check for tag-format that {version} is required. Additional context: [go/librarian:release-please-lite](http://goto.google.com/librarian:release-please-lite) 2nd part of change. Fixes #1694
1 parent 0363b5a commit b2e2c93

File tree

6 files changed

+382
-5
lines changed

6 files changed

+382
-5
lines changed

doc/state-schema.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Each object in the `libraries` list represents a single library and has the foll
2525
| `preserve_regex` | list | A list of regular expressions for files and directories to preserve during the copy and remove process. | No | Each entry must be a valid regular expression. |
2626
| `remove_regex` | list | A list of regular expressions for files and directories to remove before copying generated code. If not set, this defaults to the `source_roots`. A more specific `preserve_regex` takes precedence. | No | Each entry must be a valid regular expression. |
2727
| `release_exclude_paths` | list | A list of directories to exclude from the release. | No | Each entry must be a valid directory path. |
28-
| `tag_format` | string | A format string for the release tag. The supported placeholders are `{id}` and `{version}`. | No | Must only contain `{id}` and `{version}` as placeholders. |
28+
| `tag_format` | string | A format string for the release tag. The supported placeholders are `{id}` and `{version}`. | No | Must contain `{version}` and may optionally contain `{id}`. No other placeholders are allowed. |
2929

3030
## `apis` Object
3131

internal/config/state.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ type LibraryState struct {
110110
// Specifying a tag format allows librarian to honor this format when creating
111111
// a tag for the release of the library. The replacement values of {id} and {version}
112112
// permitted to reference the values configured in the library. If not specified
113-
// the assumed format is {id}-{version}.
113+
// the assumed format is {id}-{version}. e.g., {id}/v{version}.
114114
TagFormat string `yaml:"tag_format,omitempty" json:"tag_format,omitempty"`
115115
// An error message from the docker response.
116116
// This field is ignored when writing to state.yaml.
@@ -168,10 +168,13 @@ func (l *LibraryState) Validate() error {
168168
}
169169
}
170170
if l.TagFormat != "" {
171+
if !strings.Contains(l.TagFormat, "{version}") {
172+
return fmt.Errorf("invalid tag_format: must contain {version}")
173+
}
171174
matches := tagFormatRegex.FindAllString(l.TagFormat, -1)
172175
for _, match := range matches {
173176
if match != "{id}" && match != "{version}" {
174-
return fmt.Errorf("invalid placeholder in tag_format: %s", match)
177+
return fmt.Errorf("invalid tag_format: placeholder %s not recognized", match)
175178
}
176179
}
177180
}

internal/config/state_test.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,10 +267,31 @@ func TestLibrary_Validate(t *testing.T) {
267267
ID: "a/b",
268268
SourceRoots: []string{"src/a"},
269269
APIs: []*API{{Path: "a/b/v1"}},
270-
TagFormat: "{id}-{foo}",
270+
TagFormat: "{version}-{foo}",
271271
},
272272
wantErr: true,
273-
wantErrMsg: "invalid placeholder in tag_format",
273+
wantErrMsg: "not recognized",
274+
},
275+
{
276+
name: "invalid tag_format with id only",
277+
library: &LibraryState{
278+
ID: "a/b",
279+
SourceRoots: []string{"src/a"},
280+
APIs: []*API{{Path: "a/b/v1"}},
281+
TagFormat: "{id}v1.2.3",
282+
},
283+
wantErr: true,
284+
wantErrMsg: "must contain",
285+
},
286+
{
287+
name: "valid tag_format with version only",
288+
library: &LibraryState{
289+
ID: "a/b",
290+
SourceRoots: []string{"src/a"},
291+
APIs: []*API{{Path: "a/b/v1"}},
292+
TagFormat: "v{version}",
293+
},
294+
wantErr: false,
274295
},
275296
} {
276297
t.Run(test.name, func(t *testing.T) {

internal/librarian/mocks_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ type MockRepository struct {
169169
CommitCalls int
170170
GetCommitsForPathsSinceTagValue []*gitrepo.Commit
171171
GetCommitsForPathsSinceTagError error
172+
ChangedFilesInCommitValue []string
173+
ChangedFilesInCommitError error
172174
}
173175

174176
func (m *MockRepository) IsClean() (bool, error) {
@@ -207,3 +209,10 @@ func (m *MockRepository) GetCommitsForPathsSinceTag(paths []string, tagName stri
207209
}
208210
return m.GetCommitsForPathsSinceTagValue, nil
209211
}
212+
213+
func (m *MockRepository) ChangedFilesInCommit(hash string) ([]string, error) {
214+
if m.ChangedFilesInCommitError != nil {
215+
return nil, m.ChangedFilesInCommitError
216+
}
217+
return m.ChangedFilesInCommitValue, nil
218+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package librarian
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
21+
"github.com/googleapis/librarian/internal/config"
22+
"github.com/googleapis/librarian/internal/gitrepo"
23+
)
24+
25+
const defaultTagFormat = "{id}-{version}"
26+
27+
// GetConventionalCommitsSinceLastRelease returns all conventional commits for the given library since the
28+
// version specified in the state file.
29+
func GetConventionalCommitsSinceLastRelease(repo gitrepo.Repository, library *config.LibraryState) ([]*gitrepo.ConventionalCommit, error) {
30+
tag := formatTag(library)
31+
commits, err := repo.GetCommitsForPathsSinceTag(library.SourceRoots, tag)
32+
if err != nil {
33+
return nil, fmt.Errorf("failed to get commits for library %s: %w", library.ID, err)
34+
}
35+
var conventionalCommits []*gitrepo.ConventionalCommit
36+
for _, commit := range commits {
37+
files, err := repo.ChangedFilesInCommit(commit.Hash.String())
38+
if err != nil {
39+
return nil, fmt.Errorf("failed to get changed files for commit %s: %w", commit.Hash.String(), err)
40+
}
41+
if shouldExclude(files, library.ReleaseExcludePaths) {
42+
continue
43+
}
44+
conventionalCommit, err := gitrepo.ParseCommit(commit.Message, commit.Hash.String())
45+
if err != nil {
46+
return nil, fmt.Errorf("failed to parse commit %s: %w", commit.Hash.String(), err)
47+
}
48+
conventionalCommits = append(conventionalCommits, conventionalCommit)
49+
}
50+
return conventionalCommits, nil
51+
}
52+
53+
// shouldExclude determines if a commit should be excluded from a release.
54+
// It returns true if all files in the commit match one of the exclude paths.
55+
func shouldExclude(files, excludePaths []string) bool {
56+
for _, file := range files {
57+
excluded := false
58+
for _, excludePath := range excludePaths {
59+
if strings.HasPrefix(file, excludePath) {
60+
excluded = true
61+
break
62+
}
63+
}
64+
if !excluded {
65+
return false
66+
}
67+
}
68+
return true
69+
}
70+
71+
// formatTag returns the git tag for a given library version.
72+
func formatTag(library *config.LibraryState) string {
73+
tagFormat := library.TagFormat
74+
if tagFormat == "" {
75+
tagFormat = defaultTagFormat
76+
}
77+
r := strings.NewReplacer("{id}", library.ID, "{version}", library.Version)
78+
return r.Replace(tagFormat)
79+
}

0 commit comments

Comments
 (0)