From aa3a762401c95ffb808a615dae74d86e28a4b884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Tue, 15 Mar 2022 15:49:45 +0100 Subject: [PATCH 01/24] optimize tag-release sync procedure for pull-mirrors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For large repositories with many tags, SyncReleasesWithTags can be a costly operation (taking several minutes to complete). The reason is two-fold 1. on sync, every upstream repo tag is compared (for changes) against existing local entries in the release table to ensure that they are up-to-date. 2. the procedure for getting each tag involves several git operations git show-ref --tags -- v8.2.4477 git cat-file -t 29ab6ce9f36660cffaad3c8789e71162e5db5d2f git cat-file -p 29ab6ce9f36660cffaad3c8789e71162e5db5d2f git rev-list --count 29ab6ce9f36660cffaad3c8789e71162e5db5d2f of which the 'git rev-list --count' can be particularly heavy. This commit optimizes performance for pull-mirrors. We utilize the fact that a pull-mirror is always identical to its upstream and rebuild the entire release table on every sync and use a batch 'git for-each-ref .. refs/tags' call to retrieve all tags in one go. For large mirror repos, with hundreds of annotated tags, this brings down the duration of the sync operation from several minutes to a few seconds. Signed-off-by: Peter Gardfjäll --- modules/git/repo_tag.go | 106 +++++++++++++++++++++++++++++++------ modules/repository/repo.go | 61 +++++++++++++++++++++ 2 files changed, 151 insertions(+), 16 deletions(-) diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index d1b076ffc3f8f..8081606e8c869 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -109,37 +109,111 @@ func (repo *Repository) GetTagWithID(idStr, name string) (*Tag, error) { return tag, nil } +const ( + dualNullChar = "\x00\x00" + forEachRefTagsFormat = `type %(objecttype)%00tag %(refname:short)%00object %(object)%00objectname %(objectname)%00tagger %(creator)%00message %(contents)%00signature %(contents:signature)%00%00` +) + // GetTagInfos returns all tag infos of the repository. func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { - // TODO this a slow implementation, makes one git command per tag - stdout, err := NewCommand(repo.Ctx, "tag").RunInDir(repo.Path) + stdout, err := NewCommand(repo.Ctx, "for-each-ref", "--format", forEachRefTagsFormat, "--sort", "-*creatordate", "refs/tags").RunInDir(repo.Path) if err != nil { return nil, 0, err } - tagNames := strings.Split(strings.TrimRight(stdout, "\n"), "\n") - tagsTotal := len(tagNames) + refBlocks := strings.Split(stdout, dualNullChar) + var tags []*Tag + for _, refBlock := range refBlocks { + refBlock = strings.TrimSpace(refBlock) + if refBlock == "" { + break + } + + tag, err := parseTagRef(refBlock) + if err != nil { + return nil, 0, err + } + + tags = append(tags, tag) + } + tagsTotal := len(tags) if page != 0 { - tagNames = util.PaginateSlice(tagNames, page, pageSize).([]string) + tags = util.PaginateSlice(tags, page, pageSize).([]*Tag) } + // TODO shouldn't be necessary + sortTagsByTime(tags) + return tags, tagsTotal, nil +} - tags := make([]*Tag, 0, len(tagNames)) - for _, tagName := range tagNames { - tagName = strings.TrimSpace(tagName) - if len(tagName) == 0 { +// note: relies on output being formatted using forEachRefFormat +func parseTagRef(ref string) (*Tag, error) { + var tag Tag + items := strings.Split(ref, "\x00") + for _, item := range items { + // item = strings.TrimSpace(item) + if item == "" { continue } - tag, err := repo.GetTag(tagName) - if err != nil { - return nil, tagsTotal, err + var field string + var value string + firstSpace := strings.Index(item, " ") + if firstSpace > 0 { + field = item[:firstSpace] + value = item[firstSpace+1:] + } else { + field = item + } + + if value == "" { + continue + } + + switch field { + case "type": + tag.Type = value + case "tag": + tag.Name = value + case "objectname": + var err error + tag.ID, err = NewIDFromString(value) + if err != nil { + return nil, fmt.Errorf("parse objectname '%s': %v", value, err) + } + if tag.Type == "commit" { + tag.Object = tag.ID + } + case "object": + var err error + tag.Object, err = NewIDFromString(value) + if err != nil { + return nil, fmt.Errorf("parse object '%s': %v", value, err) + } + case "tagger": + var err error + tag.Tagger, err = newSignatureFromCommitline([]byte(value)) + if err != nil { + return nil, fmt.Errorf("parse tagger: %w", err) + } + case "message": + tag.Message = value + // srtip PGP signature if present in contents field + pgpStart := strings.Index(value, beginpgp) + if pgpStart >= 0 { + tag.Message = tag.Message[0:pgpStart] + } + // tag.Message += "\n" + case "signature": + tag.Signature = &CommitGPGSignature{ + Signature: value, + // TODO: don't know what to do about Payload. Is + // it even relevant in this context? + } } - tag.Name = tagName - tags = append(tags, tag) } - sortTagsByTime(tags) - return tags, tagsTotal, nil + + return &tag, nil } // GetAnnotatedTag returns a Git tag by its SHA, must be an annotated tag diff --git a/modules/repository/repo.go b/modules/repository/repo.go index 3ed48134c3ca4..131975cddf489 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -150,6 +150,9 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, } if !opts.Releases { + // note: this will greatly improve release (tag) sync + // for pull-mirrors with many tags + repo.IsMirror = opts.Mirror if err = SyncReleasesWithTags(repo, gitRepo); err != nil { log.Error("Failed to synchronize tags to releases for repository: %v", err) } @@ -254,6 +257,14 @@ func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo // SyncReleasesWithTags synchronizes release table with repository tags func SyncReleasesWithTags(repo *repo_model.Repository, gitRepo *git.Repository) error { + log.Debug("SyncReleasesWithTags: in Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name) + + // optimized procedure for pull-mirrors which saves a lot of time (in + // particular for repos with many tags). + if repo.IsMirror { + return pullMirrorReleaseSync(repo, gitRepo) + } + existingRelTags := make(map[string]struct{}) opts := models.FindReleasesOptions{ IncludeDrafts: true, @@ -450,3 +461,53 @@ func StoreMissingLfsObjectsInRepository(ctx context.Context, repo *repo_model.Re return nil } + +// pullMirrorReleaseSync is a pull-mirror specific tag<->release table +// synchronization which overwrites all Releases from the repository tags. This +// can be relied on since a pull-mirror is always identical to its +// upstream. Hence, after each sync we want the pull-mirror release set to be +// identical to the upstream tag set. This is much more efficient for +// repositories like https://github.com/vim/vim (with over 13000 tags). +func pullMirrorReleaseSync(repo *repo_model.Repository, gitRepo *git.Repository) error { + log.Trace("pullMirrorReleaseSync: rebuilding releases for pull-mirror Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name) + tags, numTags, err := gitRepo.GetTagInfos(0, 0) + if err != nil { + return fmt.Errorf("unable to GetTagInfos in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) + } + err = db.WithTx(func(ctx context.Context) error { + // + // clear out existing releases + // + if _, err := db.DeleteByBean(ctx, &models.Release{RepoID: repo.ID}); err != nil { + return fmt.Errorf("unable to clear releases for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) + } + // + // make release set identical to upstream tags + // + for _, tag := range tags { + release := models.Release{ + RepoID: repo.ID, + TagName: tag.Name, + LowerTagName: strings.ToLower(tag.Name), + Sha1: tag.Object.String(), + // NOTE: ignored, since NumCommits are unused + // for pull-mirrors (only relevant when + // displaying releases, IsTag: false) + NumCommits: -1, + CreatedUnix: timeutil.TimeStamp(tag.Tagger.When.Unix()), + IsTag: true, + } + if err := db.Insert(ctx, release); err != nil { + return fmt.Errorf("unable insert tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err) + } + } + return nil + + }) + if err != nil { + return fmt.Errorf("unable to rebuild release table for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) + } + + log.Trace("pullMirrorReleaseSync: done rebuilding %d releases", numTags) + return nil +} From 33f5a5e533abbc243665b3809504aca0a84b638f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Fri, 18 Mar 2022 12:11:20 +0100 Subject: [PATCH 02/24] optimize tag-release sync procedure for pull-mirrors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For large repositories with many tags, SyncReleasesWithTags can be a costly operation (taking several minutes to complete). The reason is two-fold 1. on sync, every upstream repo tag is compared (for changes) against existing local entries in the release table to ensure that they are up-to-date. 2. the procedure for getting each tag involves several git operations git show-ref --tags -- v8.2.4477 git cat-file -t 29ab6ce9f36660cffaad3c8789e71162e5db5d2f git cat-file -p 29ab6ce9f36660cffaad3c8789e71162e5db5d2f git rev-list --count 29ab6ce9f36660cffaad3c8789e71162e5db5d2f of which the 'git rev-list --count' can be particularly heavy. This commit optimizes performance for pull-mirrors. We utilize the fact that a pull-mirror is always identical to its upstream and rebuild the entire release table on every sync and use a batch 'git for-each-ref .. refs/tags' call to retrieve all tags in one go. For large mirror repos, with hundreds of annotated tags, this brings down the duration of the sync operation from several minutes to a few seconds. Signed-off-by: Peter Gardfjäll --- modules/git/foreachref/format.go | 73 +++++++++++++++++++ modules/git/foreachref/parser.go | 121 +++++++++++++++++++++++++++++++ modules/git/repo_tag.go | 120 +++++++++++++----------------- 3 files changed, 243 insertions(+), 71 deletions(-) create mode 100644 modules/git/foreachref/format.go create mode 100644 modules/git/foreachref/parser.go diff --git a/modules/git/foreachref/format.go b/modules/git/foreachref/format.go new file mode 100644 index 0000000000000..18769838d1fcf --- /dev/null +++ b/modules/git/foreachref/format.go @@ -0,0 +1,73 @@ +package foreachref + +import ( + "encoding/hex" + "fmt" + "io" + "strings" +) + +const ( + nullChar = "\x00" + dualNullChar = "\x00\x00" +) + +type Format struct { + // fieldNames hold %(fieldname)s to be passed to the '--format' flag of + // for-each-ref. See git-for-each-ref(1) for available fields. + fieldNames []string + + // fieldDelim is the character sequence that is used to separate fields + // for each reference. fieldDelim and refDelim should be selected to not + // interfere with each other and to not be present in field values. + fieldDelim string + // refDelim is the character sequence used to separate reference from + // each other in the output. fieldDelim and refDelim should be selected + // to not interfere with each other and to not be present in field + // values. + refDelim string +} + +// NewFormat creates a forEachRefFormat using the specified fieldNames. See +// git-for-each-ref(1) for available fields. +func NewFormat(fieldNames ...string) Format { + return Format{ + fieldNames: fieldNames, + fieldDelim: nullChar, + refDelim: dualNullChar, + } +} + +// Flag returns a for-each-ref --format flag value that captures the fieldNames. +func (f Format) Flag() string { + var formatFlag strings.Builder + for i, field := range f.fieldNames { + // field key and field value + formatFlag.WriteString(fmt.Sprintf("%s %%(%s)", field, field)) + + if i < len(f.fieldNames)-1 { + // note: escape delimiters to allow control characters as + // delimiters. For example, '%00' for null character or '%0a' + // for newline. + formatFlag.WriteString(f.hexEscaped(f.fieldDelim)) + } + } + formatFlag.WriteString(f.hexEscaped(f.refDelim)) + return formatFlag.String() +} + +// Parser returns a Parser capable of parsing 'git for-each-ref' output produced +// with this Format. +func (f Format) Parser(r io.Reader) *Parser { + return NewParser(r, f) +} + +// hexEscaped produces hex-escpaed characters from a string. For example, "\n\0" +// would turn into "%0a%00". +func (f Format) hexEscaped(delim string) string { + escaped := "" + for i := 0; i < len(delim); i++ { + escaped += "%" + hex.EncodeToString([]byte{delim[i]}) + } + return escaped +} diff --git a/modules/git/foreachref/parser.go b/modules/git/foreachref/parser.go new file mode 100644 index 0000000000000..8648f1910bf32 --- /dev/null +++ b/modules/git/foreachref/parser.go @@ -0,0 +1,121 @@ +package foreachref + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strings" +) + +// Parser parses 'git for-each-ref' output according to a given output Format. +type Parser struct { + // tokenizes 'git for-each-ref' output into "reference paragraphs". + scanner *bufio.Scanner + + // format represents the '--format' string that describes the expected + // 'git for-each-ref' output structure. + format Format + + // err holds the last encountered error during parsing. + err error +} + +// NewParser creates a 'git for-each-ref' output parser that will parse all +// references in the provided Reader. The references in the output are assumed +// to follow the specified Format. +func NewParser(r io.Reader, format Format) *Parser { + scanner := bufio.NewScanner(r) + + // Split input into delimiter-separated "reference blocks". + scanner.Split( + func(data []byte, atEOF bool) (advance int, token []byte, err error) { + // Scan until delimiter, marking end of reference. + delimIdx := bytes.Index(data, []byte(format.refDelim)) + if delimIdx >= 0 { + token := data[:delimIdx] + advance := delimIdx + len(format.refDelim) + return advance, token, nil + } + // If we're at EOF, we have a final, non-terminated reference. Return it. + if atEOF { + return len(data), bytes.TrimSpace(data), nil + } + // Not yet a full field. Request more data. + return 0, nil, nil + }) + + return &Parser{ + scanner: scanner, + format: format, + err: nil, + } +} + +// Next returns the next reference as a collection of key-value pairs. nil +// denotes EOF but is also returned on errors. The Err method should always be +// consulted after Next returning nil. +// +// It could, for example return something like: +// +// { "objecttype": "tag", "refname:short": "v1.16.4", "object": "f460b7543ed500e49c133c2cd85c8c55ee9dbe27" } +// +func (p *Parser) Next() map[string]string { + if !p.scanner.Scan() { + return nil + } + fields, err := p.parseRef(p.scanner.Text()) + if err != nil { + p.err = err + return nil + } + return fields +} + +// Err returns the latest encountered parsing error. +func (p *Parser) Err() error { + return p.err +} + +// parseRef parses out all key-value pairs from a single reference block, such as +// +// "objecttype tag\0refname:short v1.16.4\0object f460b7543ed500e49c133c2cd85c8c55ee9dbe27" +// +func (p *Parser) parseRef(refBlock string) (map[string]string, error) { + if refBlock == "" { + // must be at EOF + return nil, nil + } + + fieldValues := make(map[string]string) + + fields := strings.Split(refBlock, p.format.fieldDelim) + if len(fields) != len(p.format.fieldNames) { + return nil, fmt.Errorf("unexpected number of reference fields: wanted %d, was %d", + len(fields), len(p.format.fieldNames)) + } + for i, field := range fields { + field = strings.TrimSpace(field) + + var fieldKey string + var fieldVal string + firstSpace := strings.Index(field, " ") + if firstSpace > 0 { + fieldKey = field[:firstSpace] + fieldVal = field[firstSpace+1:] + } else { + // could be the case if the requested field had no value + fieldKey = field + } + + // enforce the format order of fields + if p.format.fieldNames[i] != fieldKey { + return nil, fmt.Errorf("unexpected field name at position %d: wanted: '%s', was: '%s'", + i, p.format.fieldNames[i], fieldKey) + } + + fieldValues[fieldKey] = fieldVal + } + + return fieldValues, nil +} diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 8081606e8c869..e722826f241c1 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -10,6 +10,7 @@ import ( "fmt" "strings" + "code.gitea.io/gitea/modules/git/foreachref" "code.gitea.io/gitea/modules/util" ) @@ -116,104 +117,81 @@ const ( // GetTagInfos returns all tag infos of the repository. func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { - stdout, err := NewCommand(repo.Ctx, "for-each-ref", "--format", forEachRefTagsFormat, "--sort", "-*creatordate", "refs/tags").RunInDir(repo.Path) + forEachRefFmt := foreachref.NewFormat("objecttype", "refname:short", "object", "objectname", "creator", "contents", "contents:signature") + + stdout, err := NewCommand(repo.Ctx, "for-each-ref", "--format", forEachRefFmt.Flag(), "--sort", "-*creatordate", "refs/tags").RunInDir(repo.Path) if err != nil { return nil, 0, err } - refBlocks := strings.Split(stdout, dualNullChar) var tags []*Tag - for _, refBlock := range refBlocks { - refBlock = strings.TrimSpace(refBlock) - if refBlock == "" { + parser := forEachRefFmt.Parser(strings.NewReader(stdout)) + for { + ref := parser.Next() + if ref == nil { break } - tag, err := parseTagRef(refBlock) + tag, err := parseTagRef(ref) if err != nil { - return nil, 0, err + return nil, 0, fmt.Errorf("GetTagInfos: parse tag: %w", err) } - tags = append(tags, tag) } + if err := parser.Err(); err != nil { + return nil, 0, fmt.Errorf("GetTagInfos: parse output: %w", err) + } tagsTotal := len(tags) if page != 0 { tags = util.PaginateSlice(tags, page, pageSize).([]*Tag) } - // TODO shouldn't be necessary + // TODO shouldn't be necessary since we're running with '--sort'? sortTagsByTime(tags) return tags, tagsTotal, nil } -// note: relies on output being formatted using forEachRefFormat -func parseTagRef(ref string) (*Tag, error) { - var tag Tag - items := strings.Split(ref, "\x00") - for _, item := range items { - // item = strings.TrimSpace(item) - if item == "" { - continue - } +func parseTagRef(ref map[string]string) (tag *Tag, err error) { + tag = &Tag{ + Type: ref["objecttype"], + Name: ref["refname:short"], + } - var field string - var value string - firstSpace := strings.Index(item, " ") - if firstSpace > 0 { - field = item[:firstSpace] - value = item[firstSpace+1:] - } else { - field = item - } + tag.ID, err = NewIDFromString(ref["objectname"]) + if err != nil { + return nil, fmt.Errorf("parse objectname '%s': %v", ref["objectname"], err) + } - if value == "" { - continue + if tag.Type == "commit" { + // lightweight tag + tag.Object = tag.ID + } else { + // annotated tag + tag.Object, err = NewIDFromString(ref["object"]) + if err != nil { + return nil, fmt.Errorf("parse object '%s': %v", ref["object"], err) } + } - switch field { - case "type": - tag.Type = value - case "tag": - tag.Name = value - case "objectname": - var err error - tag.ID, err = NewIDFromString(value) - if err != nil { - return nil, fmt.Errorf("parse objectname '%s': %v", value, err) - } - if tag.Type == "commit" { - tag.Object = tag.ID - } - case "object": - var err error - tag.Object, err = NewIDFromString(value) - if err != nil { - return nil, fmt.Errorf("parse object '%s': %v", value, err) - } - case "tagger": - var err error - tag.Tagger, err = newSignatureFromCommitline([]byte(value)) - if err != nil { - return nil, fmt.Errorf("parse tagger: %w", err) - } - case "message": - tag.Message = value - // srtip PGP signature if present in contents field - pgpStart := strings.Index(value, beginpgp) - if pgpStart >= 0 { - tag.Message = tag.Message[0:pgpStart] - } - // tag.Message += "\n" - case "signature": - tag.Signature = &CommitGPGSignature{ - Signature: value, - // TODO: don't know what to do about Payload. Is - // it even relevant in this context? - } - } + tag.Tagger, err = newSignatureFromCommitline([]byte(ref["creator"])) + if err != nil { + return nil, fmt.Errorf("parse tagger: %w", err) } - return &tag, nil + tag.Message = ref["contents"] + // strip PGP signature if present in contents field + pgpStart := strings.Index(tag.Message, beginpgp) + if pgpStart >= 0 { + tag.Message = tag.Message[0:pgpStart] + } + tag.Signature = &CommitGPGSignature{ + Signature: ref["contents:signature"], + // TODO: don't know what to do about Payload. Is it relevant in + // this context? + Payload: "", + } + + return tag, nil } // GetAnnotatedTag returns a Git tag by its SHA, must be an annotated tag From 5cd3114d4f5dcaf64961991bb77d0cb709cc0b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Fri, 18 Mar 2022 13:15:48 +0100 Subject: [PATCH 03/24] godoc --- modules/git/foreachref/format.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/git/foreachref/format.go b/modules/git/foreachref/format.go index 18769838d1fcf..3b93d018ae345 100644 --- a/modules/git/foreachref/format.go +++ b/modules/git/foreachref/format.go @@ -12,6 +12,8 @@ const ( dualNullChar = "\x00\x00" ) +// Format supports specifying and parsing an output format for 'git +// for-each-ref'. See See git-for-each-ref(1) for available fields. type Format struct { // fieldNames hold %(fieldname)s to be passed to the '--format' flag of // for-each-ref. See git-for-each-ref(1) for available fields. From 4ce50bed09950bb6bc923100a645412d0e72b2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Fri, 18 Mar 2022 13:37:52 +0100 Subject: [PATCH 04/24] please linter --- modules/repository/repo.go | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/repository/repo.go b/modules/repository/repo.go index 131975cddf489..f1524e9efdd70 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -502,7 +502,6 @@ func pullMirrorReleaseSync(repo *repo_model.Repository, gitRepo *git.Repository) } } return nil - }) if err != nil { return fmt.Errorf("unable to rebuild release table for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) From d87be92d778fa29f2ed399e90357ac52f3a0711b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Fri, 18 Mar 2022 13:41:32 +0100 Subject: [PATCH 05/24] please linter: copyright notices --- modules/git/foreachref/format.go | 4 ++++ modules/git/foreachref/parser.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/modules/git/foreachref/format.go b/modules/git/foreachref/format.go index 3b93d018ae345..03d09314c3812 100644 --- a/modules/git/foreachref/format.go +++ b/modules/git/foreachref/format.go @@ -1,3 +1,7 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + package foreachref import ( diff --git a/modules/git/foreachref/parser.go b/modules/git/foreachref/parser.go index 8648f1910bf32..5a311e68eb960 100644 --- a/modules/git/foreachref/parser.go +++ b/modules/git/foreachref/parser.go @@ -1,3 +1,7 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + package foreachref import ( From 0e6764619de1a8aab95d2f74ee1e58a729abfa05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 24 Mar 2022 09:06:00 +0100 Subject: [PATCH 06/24] test foreachref.Format --- modules/git/foreachref/format_test.go | 64 +++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 modules/git/foreachref/format_test.go diff --git a/modules/git/foreachref/format_test.go b/modules/git/foreachref/format_test.go new file mode 100644 index 0000000000000..9c7a96699958e --- /dev/null +++ b/modules/git/foreachref/format_test.go @@ -0,0 +1,64 @@ +package foreachref_test + +import ( + "testing" + + "code.gitea.io/gitea/modules/git/foreachref" + "github.com/stretchr/testify/require" +) + +func TestFormat_Flag(t *testing.T) { + tests := []struct { + name string + + givenFormat foreachref.Format + + wantFlag string + }{ + { + name: "references are delimited by dual null chars", + + // no reference fields requested + givenFormat: foreachref.NewFormat(), + + // only a reference delimiter field in --format + wantFlag: "%00%00", + }, + + { + name: "a field is a space-separated key-value pair", + + givenFormat: foreachref.NewFormat("refname:short"), + + // only a reference delimiter field + wantFlag: "refname:short %(refname:short)%00%00", + }, + + { + name: "fields are separated by a null char field-delimiter", + + givenFormat: foreachref.NewFormat("refname:short", "author"), + + wantFlag: "refname:short %(refname:short)%00author %(author)%00%00", + }, + + { + name: "multiple fields", + + givenFormat: foreachref.NewFormat("refname:short", "objecttype", "objectname"), + + wantFlag: "refname:short %(refname:short)%00objecttype %(objecttype)%00objectname %(objectname)%00%00", + }, + } + + for _, test := range tests { + tc := test // don't close over loop variable + t.Run(tc.name, func(t *testing.T) { + + gotFlag := tc.givenFormat.Flag() + + require.Equal(t, tc.wantFlag, gotFlag, "unexpected for-each-ref --format string. wanted: '%s', got: '%s'", tc.wantFlag, gotFlag) + + }) + } +} From 1fc0bb4d2f6ba914cbfc0c83d4d5070af7920077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 24 Mar 2022 10:55:49 +0100 Subject: [PATCH 07/24] test foreachref.Parser --- modules/git/foreachref/parser_test.go | 232 ++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 modules/git/foreachref/parser_test.go diff --git a/modules/git/foreachref/parser_test.go b/modules/git/foreachref/parser_test.go new file mode 100644 index 0000000000000..b0d889e1cc438 --- /dev/null +++ b/modules/git/foreachref/parser_test.go @@ -0,0 +1,232 @@ +package foreachref_test + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "strings" + "testing" + + "code.gitea.io/gitea/modules/git/foreachref" + "github.com/stretchr/testify/require" +) + +type refSlice = []map[string]string + +func Test(t *testing.T) { + tests := []struct { + name string + + givenFormat foreachref.Format + givenInput io.Reader + + wantRefs refSlice + wantErr bool + expectedErr error + }{ + { + name: "no references on empty input", + + givenFormat: foreachref.NewFormat("refname:short"), + givenInput: strings.NewReader(``), + + wantRefs: []map[string]string{}, + }, + + { + name: "single field requested for each reference", + + givenFormat: foreachref.NewFormat("refname:short"), + givenInput: strings.NewReader("refname:short v0.0.1\x00\x00refname:short v0.0.2\x00\x00refname:short v0.0.3\x00\x00"), + + wantRefs: []map[string]string{ + {"refname:short": "v0.0.1"}, + {"refname:short": "v0.0.2"}, + {"refname:short": "v0.0.3"}, + }, + }, + + { + name: "input ending without newline", + + givenFormat: foreachref.NewFormat("refname:short"), + givenInput: strings.NewReader("refname:short v0.0.1\x00\x00refname:short v0.0.2\x00\x00"), + + wantRefs: []map[string]string{ + {"refname:short": "v0.0.1"}, + {"refname:short": "v0.0.2"}, + }, + }, + + { + name: "input ending with newline", + + givenFormat: foreachref.NewFormat("refname:short"), + givenInput: strings.NewReader("refname:short v0.0.1\x00\x00refname:short v0.0.2\x00\x00\n"), + + wantRefs: []map[string]string{ + {"refname:short": "v0.0.1"}, + {"refname:short": "v0.0.2"}, + }, + }, + + { + name: "multiple fields requested for each reference", + + givenFormat: foreachref.NewFormat("refname:short", "objecttype", "objectname"), + givenInput: strings.NewReader( + + "refname:short v0.0.1\x00objecttype commit\x00objectname 7b2c5ac9fc04fc5efafb60700713d4fa609b777b\x00\x00" + + "refname:short v0.0.2\x00objecttype commit\x00objectname a1f051bc3eba734da4772d60e2d677f47cf93ef4\x00\x00" + + "refname:short v0.0.3\x00objecttype commit\x00objectname ef82de70bb3f60c65fb8eebacbb2d122ef517385\x00\x00", + ), + + wantRefs: []map[string]string{ + { + "refname:short": "v0.0.1", + "objecttype": "commit", + "objectname": "7b2c5ac9fc04fc5efafb60700713d4fa609b777b", + }, + { + "refname:short": "v0.0.2", + "objecttype": "commit", + "objectname": "a1f051bc3eba734da4772d60e2d677f47cf93ef4", + }, + { + "refname:short": "v0.0.3", + "objecttype": "commit", + "objectname": "ef82de70bb3f60c65fb8eebacbb2d122ef517385", + }, + }, + }, + + { + name: "must handle multi-line fields such as 'content'", + + givenFormat: foreachref.NewFormat("refname:short", "contents", "author"), + givenInput: strings.NewReader( + "refname:short v0.0.1\x00contents Create new buffer if not present yet (#549)\n\nFixes a nil dereference when ProcessFoo is used\nwith multiple commands.\x00author Foo Bar 1507832733 +0200\x00\x00" + + "refname:short v0.0.2\x00contents Update CI config (#651)\n\n\x00author John Doe 1521643174 +0000\x00\x00" + + "refname:short v0.0.3\x00contents Fixed code sample for bash completion (#687)\n\n\x00author Foo Baz 1524836750 +0200\x00\x00", + ), + + wantRefs: []map[string]string{ + { + "refname:short": "v0.0.1", + "contents": "Create new buffer if not present yet (#549)\n\nFixes a nil dereference when ProcessFoo is used\nwith multiple commands.", + "author": "Foo Bar 1507832733 +0200", + }, + { + "refname:short": "v0.0.2", + "contents": "Update CI config (#651)", + "author": "John Doe 1521643174 +0000", + }, + { + "refname:short": "v0.0.3", + "contents": "Fixed code sample for bash completion (#687)", + "author": "Foo Baz 1524836750 +0200", + }, + }, + }, + + { + name: "must handle fields without values", + + givenFormat: foreachref.NewFormat("refname:short", "object", "objecttype"), + givenInput: strings.NewReader( + "refname:short v0.0.1\x00object \x00objecttype commit\x00\x00" + + "refname:short v0.0.2\x00object \x00objecttype commit\x00\x00" + + "refname:short v0.0.3\x00object \x00objecttype commit\x00\x00", + ), + + wantRefs: []map[string]string{ + { + "refname:short": "v0.0.1", + "object": "", + "objecttype": "commit", + }, + { + "refname:short": "v0.0.2", + "object": "", + "objecttype": "commit", + }, + { + "refname:short": "v0.0.3", + "object": "", + "objecttype": "commit", + }, + }, + }, + + { + name: "must fail when the number of fields in the input doesn't match expected format", + + givenFormat: foreachref.NewFormat("refname:short", "objecttype", "objectname"), + givenInput: strings.NewReader( + "refname:short v0.0.1\x00objecttype commit\x00\x00" + + "refname:short v0.0.2\x00objecttype commit\x00\x00" + + "refname:short v0.0.3\x00objecttype commit\x00\x00", + ), + + wantErr: true, + expectedErr: errors.New("unexpected number of reference fields: wanted 2, was 3"), + }, + + // TODO errors: unexpected order of fields in input + + { + name: "must fail input fields don't match expected format", + + givenFormat: foreachref.NewFormat("refname:short", "objectname"), + givenInput: strings.NewReader( + "refname:short v0.0.1\x00objecttype commit\x00\x00" + + "refname:short v0.0.2\x00objecttype commit\x00\x00" + + "refname:short v0.0.3\x00objecttype commit\x00\x00", + ), + + wantErr: true, + expectedErr: errors.New("unexpected field name at position 1: wanted: 'objectname', was: 'objecttype'"), + }, + } + + for _, test := range tests { + tc := test // don't close over loop variable + t.Run(tc.name, func(t *testing.T) { + parser := tc.givenFormat.Parser(tc.givenInput) + + // + // parse references from input + // + gotRefs := make([]map[string]string, 0) + for { + ref := parser.Next() + if ref == nil { + break + } + gotRefs = append(gotRefs, ref) + } + err := parser.Err() + + // + // verify expectations + // + if tc.wantErr { + require.Error(t, err) + require.EqualError(t, err, tc.expectedErr.Error()) + } else { + require.NoError(t, err, "for-each-ref parser unexpectedly failed with: %v", err) + require.Equal(t, tc.wantRefs, gotRefs, "for-each-ref parser produced unexpected reference set. wanted: %v, got: %v", pretty(tc.wantRefs), pretty(gotRefs)) + } + }) + } +} + +func pretty(v interface{}) string { + data, err := json.MarshalIndent(v, "", " ") + if err != nil { + // shouldn't happen + panic(fmt.Sprintf("json-marshalling failed: %v", err)) + } + return string(data) +} From 6d30da158f441983d1578245984c0f443acb7846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 24 Mar 2022 11:36:24 +0100 Subject: [PATCH 08/24] stream git for-each-ref output to Parser --- modules/git/repo_tag.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index e722826f241c1..e4b1cd19b3332 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -8,6 +8,7 @@ package git import ( "context" "fmt" + "io" "strings" "code.gitea.io/gitea/modules/git/foreachref" @@ -119,13 +120,22 @@ const ( func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { forEachRefFmt := foreachref.NewFormat("objecttype", "refname:short", "object", "objectname", "creator", "contents", "contents:signature") - stdout, err := NewCommand(repo.Ctx, "for-each-ref", "--format", forEachRefFmt.Flag(), "--sort", "-*creatordate", "refs/tags").RunInDir(repo.Path) - if err != nil { - return nil, 0, err - } + stdoutReader, stdoutWriter := io.Pipe() + defer stdoutReader.Close() + defer stdoutWriter.Close() + stderr := strings.Builder{} + rc := &RunContext{Dir: repo.Path, Stdout: stdoutWriter, Stderr: &stderr, Timeout: -1} + + go func() { + err := NewCommand(repo.Ctx, "for-each-ref", "--format", forEachRefFmt.Flag(), "--sort", "-*creatordate", "refs/tags").RunWithContext(rc) + if err != nil { + stdoutWriter.CloseWithError(ConcatenateError(err, stderr.String())) + } + stdoutWriter.Close() + }() var tags []*Tag - parser := forEachRefFmt.Parser(strings.NewReader(stdout)) + parser := forEachRefFmt.Parser(stdoutReader) for { ref := parser.Next() if ref == nil { From 5c91e3ddf605d75a781bc311160f1b646c26a252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 24 Mar 2022 11:43:03 +0100 Subject: [PATCH 09/24] copyright header for test files --- modules/git/foreachref/format_test.go | 4 ++++ modules/git/foreachref/parser_test.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/modules/git/foreachref/format_test.go b/modules/git/foreachref/format_test.go index 9c7a96699958e..ee6ba4cf0a285 100644 --- a/modules/git/foreachref/format_test.go +++ b/modules/git/foreachref/format_test.go @@ -1,3 +1,7 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + package foreachref_test import ( diff --git a/modules/git/foreachref/parser_test.go b/modules/git/foreachref/parser_test.go index b0d889e1cc438..5926729d40c35 100644 --- a/modules/git/foreachref/parser_test.go +++ b/modules/git/foreachref/parser_test.go @@ -1,3 +1,7 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + package foreachref_test import ( From 2847d1367f9656d858e29fce9b38f86dbac90917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 24 Mar 2022 11:46:49 +0100 Subject: [PATCH 10/24] gofumpt --- modules/git/foreachref/format_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/git/foreachref/format_test.go b/modules/git/foreachref/format_test.go index ee6ba4cf0a285..aa41f757d7eff 100644 --- a/modules/git/foreachref/format_test.go +++ b/modules/git/foreachref/format_test.go @@ -58,11 +58,9 @@ func TestFormat_Flag(t *testing.T) { for _, test := range tests { tc := test // don't close over loop variable t.Run(tc.name, func(t *testing.T) { - gotFlag := tc.givenFormat.Flag() require.Equal(t, tc.wantFlag, gotFlag, "unexpected for-each-ref --format string. wanted: '%s', got: '%s'", tc.wantFlag, gotFlag) - }) } } From 5d6c962c1989de6d3beff619f442323b6fc84bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 24 Mar 2022 11:47:49 +0100 Subject: [PATCH 11/24] explicitly ignore return value --- modules/git/repo_tag.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index e4b1cd19b3332..7d14f4896d196 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -131,7 +131,7 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { if err != nil { stdoutWriter.CloseWithError(ConcatenateError(err, stderr.String())) } - stdoutWriter.Close() + _ = stdoutWriter.Close() }() var tags []*Tag From 73bb43ec1c35312f37d5e6ed410c59f18b8ca0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 24 Mar 2022 11:52:15 +0100 Subject: [PATCH 12/24] explicitly ignore return value (again) --- modules/git/repo_tag.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 7d14f4896d196..3dc821afbc6be 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -129,7 +129,7 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { go func() { err := NewCommand(repo.Ctx, "for-each-ref", "--format", forEachRefFmt.Flag(), "--sort", "-*creatordate", "refs/tags").RunWithContext(rc) if err != nil { - stdoutWriter.CloseWithError(ConcatenateError(err, stderr.String())) + _ = stdoutWriter.CloseWithError(ConcatenateError(err, stderr.String())) } _ = stdoutWriter.Close() }() From 427480471e01f69709018d31f54b6dff4cc4fb13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 24 Mar 2022 12:03:46 +0100 Subject: [PATCH 13/24] please linter: no encoding/json --- modules/git/foreachref/parser_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/git/foreachref/parser_test.go b/modules/git/foreachref/parser_test.go index 5926729d40c35..ff89a578b0620 100644 --- a/modules/git/foreachref/parser_test.go +++ b/modules/git/foreachref/parser_test.go @@ -5,7 +5,6 @@ package foreachref_test import ( - "encoding/json" "errors" "fmt" "io" @@ -13,6 +12,7 @@ import ( "testing" "code.gitea.io/gitea/modules/git/foreachref" + "code.gitea.io/gitea/modules/json" "github.com/stretchr/testify/require" ) @@ -177,8 +177,6 @@ func Test(t *testing.T) { expectedErr: errors.New("unexpected number of reference fields: wanted 2, was 3"), }, - // TODO errors: unexpected order of fields in input - { name: "must fail input fields don't match expected format", From 638d6909f44d95be7c9eb66b237cdc6aac986cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 24 Mar 2022 14:37:37 +0100 Subject: [PATCH 14/24] rename foreachref.Parser test function --- modules/git/foreachref/parser_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/git/foreachref/parser_test.go b/modules/git/foreachref/parser_test.go index ff89a578b0620..ab94a4e6aeae4 100644 --- a/modules/git/foreachref/parser_test.go +++ b/modules/git/foreachref/parser_test.go @@ -18,7 +18,7 @@ import ( type refSlice = []map[string]string -func Test(t *testing.T) { +func TestParser(t *testing.T) { tests := []struct { name string From 38a1b6693a1b8887c408a5a2fbdde1fa017106d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 24 Mar 2022 14:39:33 +0100 Subject: [PATCH 15/24] distinguish external imports --- modules/git/foreachref/parser_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/git/foreachref/parser_test.go b/modules/git/foreachref/parser_test.go index ab94a4e6aeae4..495d99448316c 100644 --- a/modules/git/foreachref/parser_test.go +++ b/modules/git/foreachref/parser_test.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/git/foreachref" "code.gitea.io/gitea/modules/json" + "github.com/stretchr/testify/require" ) From 20f6dc58ea2619f0fe5bd679e3b474a171ecaf73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 24 Mar 2022 14:39:58 +0100 Subject: [PATCH 16/24] distinguish external imports part 2 --- modules/git/foreachref/format_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/git/foreachref/format_test.go b/modules/git/foreachref/format_test.go index aa41f757d7eff..5aca10f752577 100644 --- a/modules/git/foreachref/format_test.go +++ b/modules/git/foreachref/format_test.go @@ -8,6 +8,7 @@ import ( "testing" "code.gitea.io/gitea/modules/git/foreachref" + "github.com/stretchr/testify/require" ) From a5220cf71f8c670b79060148bb700b8f889a7dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 24 Mar 2022 14:45:14 +0100 Subject: [PATCH 17/24] sort prior to pagination --- modules/git/repo_tag.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 3dc821afbc6be..2d727a17234a5 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -152,12 +152,12 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { return nil, 0, fmt.Errorf("GetTagInfos: parse output: %w", err) } + sortTagsByTime(tags) tagsTotal := len(tags) if page != 0 { tags = util.PaginateSlice(tags, page, pageSize).([]*Tag) } - // TODO shouldn't be necessary since we're running with '--sort'? - sortTagsByTime(tags) + return tags, tagsTotal, nil } From fcd1de6ea9ba6b5567feb7058db55518c9492239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Mon, 28 Mar 2022 11:04:57 +0200 Subject: [PATCH 18/24] include payload for annotated tags with signature --- modules/git/repo_tag.go | 16 ++- modules/git/repo_tag_test.go | 182 +++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 5 deletions(-) diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 2d727a17234a5..79983efdf4488 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -161,6 +161,7 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { return tags, tagsTotal, nil } +// parseTagRef parses a tag from a 'git for-each-ref'-produced reference. func parseTagRef(ref map[string]string) (tag *Tag, err error) { tag = &Tag{ Type: ref["objecttype"], @@ -194,11 +195,16 @@ func parseTagRef(ref map[string]string) (tag *Tag, err error) { if pgpStart >= 0 { tag.Message = tag.Message[0:pgpStart] } - tag.Signature = &CommitGPGSignature{ - Signature: ref["contents:signature"], - // TODO: don't know what to do about Payload. Is it relevant in - // this context? - Payload: "", + + // annotated tag with GPG signature + if tag.Type == "tag" && ref["contents:signature"] != "" { + payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s", + tag.Object, tag.Name, ref["creator"], tag.Message) + payload = strings.TrimSpace(payload) + "\n" + tag.Signature = &CommitGPGSignature{ + Signature: ref["contents:signature"], + Payload: payload, + } } return tag, nil diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go index 0e6afabb4f7c0..9d8467286205d 100644 --- a/modules/git/repo_tag_test.go +++ b/modules/git/repo_tag_test.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRepository_GetTags(t *testing.T) { @@ -195,3 +196,184 @@ func TestRepository_GetAnnotatedTag(t *testing.T) { assert.True(t, IsErrNotExist(err)) assert.Nil(t, tag4) } + +func TestRepository_parseTagRef(t *testing.T) { + tests := []struct { + name string + + givenRef map[string]string + + want *Tag + wantErr bool + expectedErr error + }{ + { + name: "lightweight tag", + + givenRef: map[string]string{ + "objecttype": "commit", + "refname:short": "v1.9.1", + // object will be empty for lightweight tags + "object": "", + "objectname": "ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889", + "creator": "Foo Bar 1565789218 +0300", + "contents": `Add changelog of v1.9.1 (#7859) + +* add changelog of v1.9.1 +* Update CHANGELOG.md +`, + "contents:signature": "", + }, + + want: &Tag{ + Name: "v1.9.1", + ID: MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"), + Object: MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"), + Type: "commit", + Tagger: parseAuthorLine(t, "Foo Bar 1565789218 +0300"), + Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n", + Signature: nil, + }, + }, + + { + name: "annotated tag", + + givenRef: map[string]string{ + "objecttype": "tag", + "refname:short": "v0.0.1", + // object will refer to commit hash for annotated tag + "object": "3325fd8a973321fd59455492976c042dde3fd1ca", + "objectname": "8c68a1f06fc59c655b7e3905b159d761e91c53c9", + "creator": "Foo Bar 1565789218 +0300", + "contents": `Add changelog of v1.9.1 (#7859) + +* add changelog of v1.9.1 +* Update CHANGELOG.md +`, + "contents:signature": "", + }, + + want: &Tag{ + Name: "v0.0.1", + ID: MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"), + Object: MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"), + Type: "tag", + Tagger: parseAuthorLine(t, "Foo Bar 1565789218 +0300"), + Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n", + Signature: nil, + }, + }, + + { + name: "annotated tag with signature", + + givenRef: map[string]string{ + "objecttype": "tag", + "refname:short": "v0.0.1", + "object": "3325fd8a973321fd59455492976c042dde3fd1ca", + "objectname": "8c68a1f06fc59c655b7e3905b159d761e91c53c9", + "creator": "Foo Bar 1565789218 +0300", + "contents": `Add changelog of v1.9.1 (#7859) + +* add changelog of v1.9.1 +* Update CHANGELOG.md +-----BEGIN PGP SIGNATURE----- + +aBCGzBAABCgAdFiEEyWRwv/q1Q6IjSv+D4IPOwzt33PoFAmI8jbIACgkQ4IPOwzt3 +3PoRuAv9FVSbPBXvzECubls9KQd7urwEvcfG20Uf79iBwifQJUv+egNQojrs6APT +T4CdIXeGRpwJZaGTUX9RWnoDO1SLXAWnc82CypWraNwrHq8Go2YeoVu0Iy3vb0EU +REdob/tXYZecMuP8AjhUR0XfdYaERYAvJ2dYsH/UkFrqDjM3V4kPXWG+R5DCaZiE +slB5U01i4Dwb/zm/ckzhUGEcOgcnpOKX8SnY5kYRVDY47dl/yJZ1u2XWir3mu60G +1geIitH7StBddHi/8rz+sJwTfcVaLjn2p59p/Dr9aGbk17GIaKq1j0pZA2lKT0Xt +f9jDqU+9vCxnKgjSDhrwN69LF2jT47ZFjEMGV/wFPOa1EBxVWpgQ/CfEolBlbUqx +yVpbxi/6AOK2lmG130e9jEZJcu+WeZUeq851WgKSEkf2d5f/JpwtSTEOlOedu6V6 +kl845zu5oE2nKM4zMQ7XrYQn538I31ps+VGQ0H8R07WrZP8WKUWugL2cU8KmXFwg +qbHDASXl +=2yGi +-----END PGP SIGNATURE----- + +`, + "contents:signature": `-----BEGIN PGP SIGNATURE----- + +aBCGzBAABCgAdFiEEyWRwv/q1Q6IjSv+D4IPOwzt33PoFAmI8jbIACgkQ4IPOwzt3 +3PoRuAv9FVSbPBXvzECubls9KQd7urwEvcfG20Uf79iBwifQJUv+egNQojrs6APT +T4CdIXeGRpwJZaGTUX9RWnoDO1SLXAWnc82CypWraNwrHq8Go2YeoVu0Iy3vb0EU +REdob/tXYZecMuP8AjhUR0XfdYaERYAvJ2dYsH/UkFrqDjM3V4kPXWG+R5DCaZiE +slB5U01i4Dwb/zm/ckzhUGEcOgcnpOKX8SnY5kYRVDY47dl/yJZ1u2XWir3mu60G +1geIitH7StBddHi/8rz+sJwTfcVaLjn2p59p/Dr9aGbk17GIaKq1j0pZA2lKT0Xt +f9jDqU+9vCxnKgjSDhrwN69LF2jT47ZFjEMGV/wFPOa1EBxVWpgQ/CfEolBlbUqx +yVpbxi/6AOK2lmG130e9jEZJcu+WeZUeq851WgKSEkf2d5f/JpwtSTEOlOedu6V6 +kl845zu5oE2nKM4zMQ7XrYQn538I31ps+VGQ0H8R07WrZP8WKUWugL2cU8KmXFwg +qbHDASXl +=2yGi +-----END PGP SIGNATURE----- + +`, + }, + + want: &Tag{ + Name: "v0.0.1", + ID: MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"), + Object: MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"), + Type: "tag", + Tagger: parseAuthorLine(t, "Foo Bar 1565789218 +0300"), + Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md", + Signature: &CommitGPGSignature{ + Signature: `-----BEGIN PGP SIGNATURE----- + +aBCGzBAABCgAdFiEEyWRwv/q1Q6IjSv+D4IPOwzt33PoFAmI8jbIACgkQ4IPOwzt3 +3PoRuAv9FVSbPBXvzECubls9KQd7urwEvcfG20Uf79iBwifQJUv+egNQojrs6APT +T4CdIXeGRpwJZaGTUX9RWnoDO1SLXAWnc82CypWraNwrHq8Go2YeoVu0Iy3vb0EU +REdob/tXYZecMuP8AjhUR0XfdYaERYAvJ2dYsH/UkFrqDjM3V4kPXWG+R5DCaZiE +slB5U01i4Dwb/zm/ckzhUGEcOgcnpOKX8SnY5kYRVDY47dl/yJZ1u2XWir3mu60G +1geIitH7StBddHi/8rz+sJwTfcVaLjn2p59p/Dr9aGbk17GIaKq1j0pZA2lKT0Xt +f9jDqU+9vCxnKgjSDhrwN69LF2jT47ZFjEMGV/wFPOa1EBxVWpgQ/CfEolBlbUqx +yVpbxi/6AOK2lmG130e9jEZJcu+WeZUeq851WgKSEkf2d5f/JpwtSTEOlOedu6V6 +kl845zu5oE2nKM4zMQ7XrYQn538I31ps+VGQ0H8R07WrZP8WKUWugL2cU8KmXFwg +qbHDASXl +=2yGi +-----END PGP SIGNATURE----- + +`, + Payload: `object 3325fd8a973321fd59455492976c042dde3fd1ca +type commit +tag v0.0.1 +tagger Foo Bar 1565789218 +0300 + +Add changelog of v1.9.1 (#7859) + +* add changelog of v1.9.1 +* Update CHANGELOG.md +`, + }, + }, + }, + } + + for _, test := range tests { + tc := test // don't close over loop variable + t.Run(tc.name, func(t *testing.T) { + got, err := parseTagRef(tc.givenRef) + + if tc.wantErr { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedErr) + } else { + require.NoError(t, err) + require.Equal(t, tc.want, got) + } + }) + } +} + +func parseAuthorLine(t *testing.T, committer string) *Signature { + t.Helper() + + sig, err := newSignatureFromCommitline([]byte(committer)) + if err != nil { + t.Fatalf("parse author line '%s': %v", committer, err) + } + + return sig +} From ef6352c0839f6e298183ee683abb20f4483ee521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Mon, 28 Mar 2022 15:00:25 +0200 Subject: [PATCH 19/24] less verbose signature payload construction --- modules/git/repo_tag.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 79983efdf4488..3914c6aa07c22 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -198,9 +198,8 @@ func parseTagRef(ref map[string]string) (tag *Tag, err error) { // annotated tag with GPG signature if tag.Type == "tag" && ref["contents:signature"] != "" { - payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s", - tag.Object, tag.Name, ref["creator"], tag.Message) - payload = strings.TrimSpace(payload) + "\n" + payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s\n", + tag.Object, tag.Name, ref["creator"], strings.TrimSpace(tag.Message)) tag.Signature = &CommitGPGSignature{ Signature: ref["contents:signature"], Payload: payload, From 2f3908c2930beb87f8aff77af21ccf4ff4d20095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 31 Mar 2022 08:42:42 +0200 Subject: [PATCH 20/24] code cleanup: remove dead code --- modules/git/repo_tag.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 3914c6aa07c22..b6bc905752880 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -111,11 +111,6 @@ func (repo *Repository) GetTagWithID(idStr, name string) (*Tag, error) { return tag, nil } -const ( - dualNullChar = "\x00\x00" - forEachRefTagsFormat = `type %(objecttype)%00tag %(refname:short)%00object %(object)%00objectname %(objectname)%00tagger %(creator)%00message %(contents)%00signature %(contents:signature)%00%00` -) - // GetTagInfos returns all tag infos of the repository. func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { forEachRefFmt := foreachref.NewFormat("objecttype", "refname:short", "object", "objectname", "creator", "contents", "contents:signature") From aa267d2ea87ee6ffa59858bc63c86346c16086e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 31 Mar 2022 11:48:23 +0200 Subject: [PATCH 21/24] delimiters can be byte slices --- modules/git/foreachref/format.go | 12 ++++++------ modules/git/foreachref/parser.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/git/foreachref/format.go b/modules/git/foreachref/format.go index 03d09314c3812..d43213052c274 100644 --- a/modules/git/foreachref/format.go +++ b/modules/git/foreachref/format.go @@ -11,9 +11,9 @@ import ( "strings" ) -const ( - nullChar = "\x00" - dualNullChar = "\x00\x00" +var ( + nullChar = []byte("\x00") + dualNullChar = []byte("\x00\x00") ) // Format supports specifying and parsing an output format for 'git @@ -26,12 +26,12 @@ type Format struct { // fieldDelim is the character sequence that is used to separate fields // for each reference. fieldDelim and refDelim should be selected to not // interfere with each other and to not be present in field values. - fieldDelim string + fieldDelim []byte // refDelim is the character sequence used to separate reference from // each other in the output. fieldDelim and refDelim should be selected // to not interfere with each other and to not be present in field // values. - refDelim string + refDelim []byte } // NewFormat creates a forEachRefFormat using the specified fieldNames. See @@ -70,7 +70,7 @@ func (f Format) Parser(r io.Reader) *Parser { // hexEscaped produces hex-escpaed characters from a string. For example, "\n\0" // would turn into "%0a%00". -func (f Format) hexEscaped(delim string) string { +func (f Format) hexEscaped(delim []byte) string { escaped := "" for i := 0; i < len(delim); i++ { escaped += "%" + hex.EncodeToString([]byte{delim[i]}) diff --git a/modules/git/foreachref/parser.go b/modules/git/foreachref/parser.go index 5a311e68eb960..723466e158de5 100644 --- a/modules/git/foreachref/parser.go +++ b/modules/git/foreachref/parser.go @@ -93,7 +93,7 @@ func (p *Parser) parseRef(refBlock string) (map[string]string, error) { fieldValues := make(map[string]string) - fields := strings.Split(refBlock, p.format.fieldDelim) + fields := strings.Split(refBlock, string(p.format.fieldDelim)) if len(fields) != len(p.format.fieldNames) { return nil, fmt.Errorf("unexpected number of reference fields: wanted %d, was %d", len(fields), len(p.format.fieldNames)) From 61eae9e9202c23531fdecee5ad4bff64c0de2e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 31 Mar 2022 11:49:53 +0200 Subject: [PATCH 22/24] avoid closing writer twice --- modules/git/repo_tag.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index b6bc905752880..8886ad0a6b3e2 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -125,8 +125,9 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { err := NewCommand(repo.Ctx, "for-each-ref", "--format", forEachRefFmt.Flag(), "--sort", "-*creatordate", "refs/tags").RunWithContext(rc) if err != nil { _ = stdoutWriter.CloseWithError(ConcatenateError(err, stderr.String())) + } else { + _ = stdoutWriter.Close() } - _ = stdoutWriter.Close() }() var tags []*Tag From fd7f58c077866220883d36f9cb1513a1e1d03b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 31 Mar 2022 12:58:30 +0200 Subject: [PATCH 23/24] avoid repetitive memory allocations --- modules/git/foreachref/format.go | 11 ++++++++--- modules/git/foreachref/parser.go | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/git/foreachref/format.go b/modules/git/foreachref/format.go index d43213052c274..c9aa5233e1e2a 100644 --- a/modules/git/foreachref/format.go +++ b/modules/git/foreachref/format.go @@ -27,6 +27,10 @@ type Format struct { // for each reference. fieldDelim and refDelim should be selected to not // interfere with each other and to not be present in field values. fieldDelim []byte + // fieldDelimStr is a string representation of fieldDelim. Used to save + // us from repetitive reallocation whenever we need the delimiter as a + // string. + fieldDelimStr string // refDelim is the character sequence used to separate reference from // each other in the output. fieldDelim and refDelim should be selected // to not interfere with each other and to not be present in field @@ -38,9 +42,10 @@ type Format struct { // git-for-each-ref(1) for available fields. func NewFormat(fieldNames ...string) Format { return Format{ - fieldNames: fieldNames, - fieldDelim: nullChar, - refDelim: dualNullChar, + fieldNames: fieldNames, + fieldDelim: nullChar, + fieldDelimStr: string(nullChar), + refDelim: dualNullChar, } } diff --git a/modules/git/foreachref/parser.go b/modules/git/foreachref/parser.go index 723466e158de5..bb2cd4d027938 100644 --- a/modules/git/foreachref/parser.go +++ b/modules/git/foreachref/parser.go @@ -35,7 +35,7 @@ func NewParser(r io.Reader, format Format) *Parser { scanner.Split( func(data []byte, atEOF bool) (advance int, token []byte, err error) { // Scan until delimiter, marking end of reference. - delimIdx := bytes.Index(data, []byte(format.refDelim)) + delimIdx := bytes.Index(data, format.refDelim) if delimIdx >= 0 { token := data[:delimIdx] advance := delimIdx + len(format.refDelim) @@ -93,7 +93,7 @@ func (p *Parser) parseRef(refBlock string) (map[string]string, error) { fieldValues := make(map[string]string) - fields := strings.Split(refBlock, string(p.format.fieldDelim)) + fields := strings.Split(refBlock, p.format.fieldDelimStr) if len(fields) != len(p.format.fieldNames) { return nil, fmt.Errorf("unexpected number of reference fields: wanted %d, was %d", len(fields), len(p.format.fieldNames)) From c1da4e0aee43ca0f478f30b9edf9f9e326bcf624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Gardfj=C3=A4ll?= Date: Thu, 31 Mar 2022 13:39:42 +0200 Subject: [PATCH 24/24] make sure tests add newline to every reference in simulated output --- modules/git/foreachref/parser.go | 12 ++++-- modules/git/foreachref/parser_test.go | 61 ++++++++++++--------------- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/modules/git/foreachref/parser.go b/modules/git/foreachref/parser.go index bb2cd4d027938..eb8b77d9038bf 100644 --- a/modules/git/foreachref/parser.go +++ b/modules/git/foreachref/parser.go @@ -31,19 +31,25 @@ type Parser struct { func NewParser(r io.Reader, format Format) *Parser { scanner := bufio.NewScanner(r) + // in addition to the reference delimiter we specified in the --format, + // `git for-each-ref` will always add a newline after every reference. + refDelim := make([]byte, 0, len(format.refDelim)+1) + refDelim = append(refDelim, format.refDelim...) + refDelim = append(refDelim, '\n') + // Split input into delimiter-separated "reference blocks". scanner.Split( func(data []byte, atEOF bool) (advance int, token []byte, err error) { // Scan until delimiter, marking end of reference. - delimIdx := bytes.Index(data, format.refDelim) + delimIdx := bytes.Index(data, refDelim) if delimIdx >= 0 { token := data[:delimIdx] - advance := delimIdx + len(format.refDelim) + advance := delimIdx + len(refDelim) return advance, token, nil } // If we're at EOF, we have a final, non-terminated reference. Return it. if atEOF { - return len(data), bytes.TrimSpace(data), nil + return len(data), data, nil } // Not yet a full field. Request more data. return 0, nil, nil diff --git a/modules/git/foreachref/parser_test.go b/modules/git/foreachref/parser_test.go index 495d99448316c..cb36428604a60 100644 --- a/modules/git/foreachref/parser_test.go +++ b/modules/git/foreachref/parser_test.go @@ -30,6 +30,8 @@ func TestParser(t *testing.T) { wantErr bool expectedErr error }{ + // this would, for example, be the result when running `git + // for-each-ref refs/tags` on a repo without tags. { name: "no references on empty input", @@ -39,40 +41,31 @@ func TestParser(t *testing.T) { wantRefs: []map[string]string{}, }, + // note: `git for-each-ref` will add a newline between every + // reference (in addition to the ref-delimiter we've chosen) { - name: "single field requested for each reference", + name: "single field requested, single reference in output", givenFormat: foreachref.NewFormat("refname:short"), - givenInput: strings.NewReader("refname:short v0.0.1\x00\x00refname:short v0.0.2\x00\x00refname:short v0.0.3\x00\x00"), + givenInput: strings.NewReader("refname:short v0.0.1\x00\x00" + "\n"), wantRefs: []map[string]string{ {"refname:short": "v0.0.1"}, - {"refname:short": "v0.0.2"}, - {"refname:short": "v0.0.3"}, }, }, - { - name: "input ending without newline", + name: "single field requested, multiple references in output", givenFormat: foreachref.NewFormat("refname:short"), - givenInput: strings.NewReader("refname:short v0.0.1\x00\x00refname:short v0.0.2\x00\x00"), - - wantRefs: []map[string]string{ - {"refname:short": "v0.0.1"}, - {"refname:short": "v0.0.2"}, - }, - }, - - { - name: "input ending with newline", - - givenFormat: foreachref.NewFormat("refname:short"), - givenInput: strings.NewReader("refname:short v0.0.1\x00\x00refname:short v0.0.2\x00\x00\n"), + givenInput: strings.NewReader( + "refname:short v0.0.1\x00\x00" + "\n" + + "refname:short v0.0.2\x00\x00" + "\n" + + "refname:short v0.0.3\x00\x00" + "\n"), wantRefs: []map[string]string{ {"refname:short": "v0.0.1"}, {"refname:short": "v0.0.2"}, + {"refname:short": "v0.0.3"}, }, }, @@ -82,9 +75,9 @@ func TestParser(t *testing.T) { givenFormat: foreachref.NewFormat("refname:short", "objecttype", "objectname"), givenInput: strings.NewReader( - "refname:short v0.0.1\x00objecttype commit\x00objectname 7b2c5ac9fc04fc5efafb60700713d4fa609b777b\x00\x00" + - "refname:short v0.0.2\x00objecttype commit\x00objectname a1f051bc3eba734da4772d60e2d677f47cf93ef4\x00\x00" + - "refname:short v0.0.3\x00objecttype commit\x00objectname ef82de70bb3f60c65fb8eebacbb2d122ef517385\x00\x00", + "refname:short v0.0.1\x00objecttype commit\x00objectname 7b2c5ac9fc04fc5efafb60700713d4fa609b777b\x00\x00" + "\n" + + "refname:short v0.0.2\x00objecttype commit\x00objectname a1f051bc3eba734da4772d60e2d677f47cf93ef4\x00\x00" + "\n" + + "refname:short v0.0.3\x00objecttype commit\x00objectname ef82de70bb3f60c65fb8eebacbb2d122ef517385\x00\x00" + "\n", ), wantRefs: []map[string]string{ @@ -111,9 +104,9 @@ func TestParser(t *testing.T) { givenFormat: foreachref.NewFormat("refname:short", "contents", "author"), givenInput: strings.NewReader( - "refname:short v0.0.1\x00contents Create new buffer if not present yet (#549)\n\nFixes a nil dereference when ProcessFoo is used\nwith multiple commands.\x00author Foo Bar 1507832733 +0200\x00\x00" + - "refname:short v0.0.2\x00contents Update CI config (#651)\n\n\x00author John Doe 1521643174 +0000\x00\x00" + - "refname:short v0.0.3\x00contents Fixed code sample for bash completion (#687)\n\n\x00author Foo Baz 1524836750 +0200\x00\x00", + "refname:short v0.0.1\x00contents Create new buffer if not present yet (#549)\n\nFixes a nil dereference when ProcessFoo is used\nwith multiple commands.\x00author Foo Bar 1507832733 +0200\x00\x00" + "\n" + + "refname:short v0.0.2\x00contents Update CI config (#651)\n\n\x00author John Doe 1521643174 +0000\x00\x00" + "\n" + + "refname:short v0.0.3\x00contents Fixed code sample for bash completion (#687)\n\n\x00author Foo Baz 1524836750 +0200\x00\x00" + "\n", ), wantRefs: []map[string]string{ @@ -140,9 +133,9 @@ func TestParser(t *testing.T) { givenFormat: foreachref.NewFormat("refname:short", "object", "objecttype"), givenInput: strings.NewReader( - "refname:short v0.0.1\x00object \x00objecttype commit\x00\x00" + - "refname:short v0.0.2\x00object \x00objecttype commit\x00\x00" + - "refname:short v0.0.3\x00object \x00objecttype commit\x00\x00", + "refname:short v0.0.1\x00object \x00objecttype commit\x00\x00" + "\n" + + "refname:short v0.0.2\x00object \x00objecttype commit\x00\x00" + "\n" + + "refname:short v0.0.3\x00object \x00objecttype commit\x00\x00" + "\n", ), wantRefs: []map[string]string{ @@ -169,9 +162,9 @@ func TestParser(t *testing.T) { givenFormat: foreachref.NewFormat("refname:short", "objecttype", "objectname"), givenInput: strings.NewReader( - "refname:short v0.0.1\x00objecttype commit\x00\x00" + - "refname:short v0.0.2\x00objecttype commit\x00\x00" + - "refname:short v0.0.3\x00objecttype commit\x00\x00", + "refname:short v0.0.1\x00objecttype commit\x00\x00" + "\n" + + "refname:short v0.0.2\x00objecttype commit\x00\x00" + "\n" + + "refname:short v0.0.3\x00objecttype commit\x00\x00" + "\n", ), wantErr: true, @@ -183,9 +176,9 @@ func TestParser(t *testing.T) { givenFormat: foreachref.NewFormat("refname:short", "objectname"), givenInput: strings.NewReader( - "refname:short v0.0.1\x00objecttype commit\x00\x00" + - "refname:short v0.0.2\x00objecttype commit\x00\x00" + - "refname:short v0.0.3\x00objecttype commit\x00\x00", + "refname:short v0.0.1\x00objecttype commit\x00\x00" + "\n" + + "refname:short v0.0.2\x00objecttype commit\x00\x00" + "\n" + + "refname:short v0.0.3\x00objecttype commit\x00\x00" + "\n", ), wantErr: true,