mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-23 12:21:53 +02:00
`published_at` was an alias for `created_at`, so a release created from an existing tag reported that tag's commit date as its publication time, and drafts reported one despite never having been published. It is now stored separately, set when a release is published and null for drafts. `created_at` in turn means the date of the commit the release points at, matching what GitHub documents it to be, and the latest release is selected by it again. Publishing a release for an old commit no longer takes over the latest badge, and a tag created in the web UI is dated the same way as one pushed from the CLI. Fixes https://github.com/go-gitea/gitea/issues/11206 Fixes https://github.com/go-gitea/gitea/issues/38714 Fixes https://github.com/go-gitea/gitea/issues/31789 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
136 lines
4.1 KiB
Go
136 lines
4.1 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
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)
|
|
|
|
// default Scanner.MaxScanTokenSize = 64 kiB may be too small for some references,
|
|
// so allow the buffer to be large enough in case the ref has long content (e.g.: a tag with long message)
|
|
// as long as it doesn't exceed some reasonable limit (4 MiB here, or MAX_DISPLAY_FILE_SIZE=8MiB), it is OK
|
|
// there are still some choices: 1. add a config option for the limit; 2. don't use scanner and write our own parser to fully handle large contents
|
|
scanner.Buffer(nil, 4*1024*1024)
|
|
|
|
// 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, refDelim)
|
|
if delimIdx >= 0 {
|
|
token := data[:delimIdx]
|
|
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), 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() {
|
|
if err := p.scanner.Err(); err != nil {
|
|
p.err = err
|
|
}
|
|
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 //nolint:nilnil // return nil to signal EOF
|
|
}
|
|
|
|
fieldValues := make(map[string]string, len(p.format.fieldNames))
|
|
|
|
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))
|
|
}
|
|
for i, field := range fields {
|
|
var fieldKey string
|
|
var fieldVal string
|
|
before, after, ok := strings.Cut(field, " ")
|
|
if ok {
|
|
fieldKey = before
|
|
fieldVal = after
|
|
} 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
|
|
}
|