fix(migrations): preserve SHA-256 pull request commit IDs (#39343)

* Fixes #39339

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
breken
2026-09-19 05:41:41 -07:00
committed by GitHub
parent 2a3d047339
commit cc34c26172
17 changed files with 105 additions and 128 deletions
+8 -5
View File
@@ -72,12 +72,14 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
}
var objectID string
objectFormatLength := r.objectFormat.FullLength()
if len(lineBytes) > objectFormatLength && lineBytes[objectFormatLength] == ' ' && r.objectFormat.IsValid(string(lineBytes[0:objectFormatLength])) {
objectID = string(lineBytes[0:objectFormatLength])
if lineField1, _, ok := bytes.Cut(lineBytes, []byte(" ")); ok {
lineFieldStr := string(lineField1)
if IsStringValidObjectID(r.objectFormat, lineFieldStr) {
objectID = lineFieldStr
}
}
if len(objectID) > 0 {
if objectID != "" {
if blamePart == nil {
blamePart = &BlamePart{
Sha: objectID,
@@ -99,6 +101,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
} else if lineBytes[0] == '\t' {
blamePart.Lines = append(blamePart.Lines, string(lineBytes[1:]))
} else if bytes.HasPrefix(lineBytes, []byte(previousHeader)) {
objectFormatLength := r.objectFormat.FullLength()
offset := len(previousHeader) // already includes a space
blamePart.PreviousSha = string(lineBytes[offset : offset+objectFormatLength])
offset += objectFormatLength + 1 // +1 for space
+1 -3
View File
@@ -4,7 +4,6 @@
package git
import (
"context"
"testing"
"gitea.dev/modules/setting"
@@ -14,8 +13,7 @@ import (
func TestReadingBlameOutputSha256(t *testing.T) {
setting.AppDataPath = t.TempDir()
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
ctx := t.Context()
if DefaultFeatures().UsingGogit {
t.Skip("Skipping test since gogit does not support sha256")
+1 -3
View File
@@ -4,7 +4,6 @@
package git
import (
"context"
"testing"
"gitea.dev/modules/setting"
@@ -14,8 +13,7 @@ import (
func TestReadingBlameOutput(t *testing.T) {
setting.AppDataPath = t.TempDir()
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
ctx := t.Context()
t.Run("Without .git-blame-ignore-revs", func(t *testing.T) {
storage := mockRepository("repo5_pulls")
-22
View File
@@ -248,25 +248,3 @@ func GetFullCommitID(ctx context.Context, repo RepositoryFacade, shortID string)
}
return strings.TrimSpace(commitID), nil
}
func IsStringLikelyCommitID(objFmt ObjectFormat, s string, minLength ...int) bool {
maxLen := 64 // sha256
if objFmt != nil {
maxLen = objFmt.FullLength()
}
minLen := util.OptionalArg(minLength, maxLen)
if len(s) < minLen || len(s) > maxLen {
return false
}
return isStringLowerHex(s)
}
func isStringLowerHex(s string) bool {
for _, c := range s {
isHex := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')
if !isHex {
return false
}
}
return len(s) > 0 // it accepts odd length because "shorten commit id" can be 7-chars
}
-7
View File
@@ -195,10 +195,3 @@ func Test_GetCommitBranchStart(t *testing.T) {
assert.NotEmpty(t, startCommitID)
assert.Equal(t, "95bb4d39648ee7e325106df01a621c530863a653", startCommitID)
}
func TestIsStringLikelyCommitID(t *testing.T) {
assert.True(t, IsStringLikelyCommitID(nil, "abc", 3))
assert.False(t, IsStringLikelyCommitID(nil, "abc", 4))
assert.True(t, IsStringLikelyCommitID(nil, strings.Repeat("a", 64), 4))
assert.False(t, IsStringLikelyCommitID(nil, strings.Repeat("a", 65), 4))
}
+14 -62
View File
@@ -6,16 +6,9 @@ package git
import (
"crypto/sha1"
"crypto/sha256"
"regexp"
"strconv"
)
// sha1Pattern can be used to determine if a string is a valid sha
var sha1Pattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`)
// sha256Pattern can be used to determine if a string is a valid sha
var sha256Pattern = regexp.MustCompile(`^[0-9a-f]{4,64}$`)
type ObjectFormat interface {
// Name returns the name of the object format
Name() string
@@ -25,8 +18,6 @@ type ObjectFormat interface {
EmptyTree() ObjectID
// FullLength is the length of the hash's hex string
FullLength() int
// IsValid returns true if the input is a valid hash
IsValid(input string) bool
// MustID creates a new ObjectID from a byte slice
MustID(b []byte) ObjectID
// ComputeHash compute the hash for a given ObjectType and content
@@ -41,19 +32,10 @@ var (
emptySha1Tree = &Sha1Hash{0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60, 0xe5, 0x4b, 0xf8, 0xd6, 0x92, 0x88, 0xfb, 0xee, 0x49, 0x04}
)
func (Sha1ObjectFormatImpl) Name() string { return "sha1" }
func (Sha1ObjectFormatImpl) EmptyObjectID() ObjectID {
return emptySha1ObjectID
}
func (Sha1ObjectFormatImpl) EmptyTree() ObjectID {
return emptySha1Tree
}
func (Sha1ObjectFormatImpl) FullLength() int { return 40 }
func (Sha1ObjectFormatImpl) IsValid(input string) bool {
return sha1Pattern.MatchString(input)
}
func (Sha1ObjectFormatImpl) Name() string { return "sha1" }
func (Sha1ObjectFormatImpl) EmptyObjectID() ObjectID { return emptySha1ObjectID }
func (Sha1ObjectFormatImpl) EmptyTree() ObjectID { return emptySha1Tree }
func (Sha1ObjectFormatImpl) FullLength() int { return 40 }
func (Sha1ObjectFormatImpl) MustID(b []byte) ObjectID {
var id Sha1Hash
copy(id[0:20], b)
@@ -83,19 +65,10 @@ var (
}
)
func (Sha256ObjectFormatImpl) Name() string { return "sha256" }
func (Sha256ObjectFormatImpl) EmptyObjectID() ObjectID {
return emptySha256ObjectID
}
func (Sha256ObjectFormatImpl) EmptyTree() ObjectID {
return emptySha256Tree
}
func (Sha256ObjectFormatImpl) FullLength() int { return 64 }
func (Sha256ObjectFormatImpl) IsValid(input string) bool {
return sha256Pattern.MatchString(input)
}
func (Sha256ObjectFormatImpl) Name() string { return "sha256" }
func (Sha256ObjectFormatImpl) EmptyObjectID() ObjectID { return emptySha256ObjectID }
func (Sha256ObjectFormatImpl) EmptyTree() ObjectID { return emptySha256Tree }
func (Sha256ObjectFormatImpl) FullLength() int { return 64 }
func (Sha256ObjectFormatImpl) MustID(b []byte) ObjectID {
var id Sha256Hash
copy(id[0:32], b)
@@ -115,34 +88,13 @@ func (h Sha256ObjectFormatImpl) ComputeHash(t ObjectType, content []byte) Object
type invalidObjectFormatImpl struct{}
var emptyInvalidObjectID = &Sha1Hash{}
func (h invalidObjectFormatImpl) Name() string {
return "invalid-object-format"
}
func (h invalidObjectFormatImpl) EmptyObjectID() ObjectID {
return emptyInvalidObjectID
}
func (h invalidObjectFormatImpl) EmptyTree() ObjectID {
return emptyInvalidObjectID
}
func (h invalidObjectFormatImpl) FullLength() int {
return len(emptyInvalidObjectID) * 2
}
func (h invalidObjectFormatImpl) IsValid(input string) bool {
return false
}
func (h invalidObjectFormatImpl) MustID(b []byte) ObjectID {
return emptyInvalidObjectID
}
func (h invalidObjectFormatImpl) Name() string { return "invalid-object-format" }
func (h invalidObjectFormatImpl) EmptyObjectID() ObjectID { return &Sha1Hash{} }
func (h invalidObjectFormatImpl) EmptyTree() ObjectID { return h.EmptyObjectID() }
func (h invalidObjectFormatImpl) FullLength() int { return len(h.EmptyObjectID().RawValue()) * 2 }
func (h invalidObjectFormatImpl) MustID(b []byte) ObjectID { return h.EmptyObjectID() }
func (h invalidObjectFormatImpl) ComputeHash(t ObjectType, content []byte) ObjectID {
return emptyInvalidObjectID
return h.EmptyObjectID()
}
var (
+36
View File
@@ -7,6 +7,8 @@ import (
"bytes"
"encoding/hex"
"fmt"
"gitea.dev/modules/util"
)
type ObjectID interface {
@@ -104,3 +106,37 @@ func IsEmptyCommitID(commitID string) bool {
func ComputeBlobHash(hashType ObjectFormat, content []byte) ObjectID {
return hashType.ComputeHash(ObjectBlob, content)
}
func IsStringValidObjectID(objFmt ObjectFormat, s string, optMinLen ...int) bool {
if objFmt == invalidObjectFormat {
return false
}
var minLen, maxLen int
if objFmt != nil {
maxLen = objFmt.FullLength()
minLen = util.OptionalArg(optMinLen, maxLen)
} else {
if len(optMinLen) == 0 {
// if no "min length" is applied, then the length must exactly match one of the formats
if len(s) != Sha1ObjectFormat.FullLength() && len(s) != Sha256ObjectFormat.FullLength() {
return false
}
}
maxLen = Sha256ObjectFormat.FullLength()
minLen = util.OptionalArg(optMinLen, Sha1ObjectFormat.FullLength())
}
if len(s) < minLen || len(s) > maxLen {
return false
}
return isStringLowerHex(s)
}
func isStringLowerHex(s string) bool {
for _, c := range s {
isHex := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')
if !isHex {
return false
}
}
return len(s) > 0 // it accepts odd length because "shorten commit id" can be 7-chars
}
+28 -7
View File
@@ -4,19 +4,13 @@
package git
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsValidSHAPattern(t *testing.T) {
assert.True(t, Sha1ObjectFormat.IsValid("fee1"))
assert.True(t, Sha1ObjectFormat.IsValid("abc000"))
assert.True(t, Sha1ObjectFormat.IsValid("9023902390239023902390239023902390239023"))
assert.False(t, Sha1ObjectFormat.IsValid("90239023902390239023902390239023902390239023"))
assert.False(t, Sha1ObjectFormat.IsValid("abc"))
assert.False(t, Sha1ObjectFormat.IsValid("123g"))
assert.False(t, Sha1ObjectFormat.IsValid("some random text"))
assert.Equal(t, "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", ComputeBlobHash(Sha1ObjectFormat, nil).String())
assert.Equal(t, "2e65efe2a145dda7ee51d1741299f848e5bf752e", ComputeBlobHash(Sha1ObjectFormat, []byte("a")).String())
assert.Equal(t, "473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813", ComputeBlobHash(Sha256ObjectFormat, nil).String())
@@ -30,3 +24,30 @@ func TestInvalidObjectFormat(t *testing.T) {
assert.NotNil(t, of)
assert.False(t, IsValidObjectFormat("no-such"))
}
func TestIsStringValidObjectID(t *testing.T) {
assert.True(t, IsStringValidObjectID(nil, "abc", 3))
assert.False(t, IsStringValidObjectID(nil, "abg", 3))
assert.False(t, IsStringValidObjectID(nil, "abc", 4))
assert.True(t, IsStringValidObjectID(nil, "0123456789abcdef", 4))
assert.False(t, IsStringValidObjectID(nil, strings.Repeat("a", 3), 4))
assert.True(t, IsStringValidObjectID(nil, strings.Repeat("a", 4), 4))
assert.True(t, IsStringValidObjectID(nil, strings.Repeat("a", 5), 4))
assert.True(t, IsStringValidObjectID(nil, strings.Repeat("a", 64), 4))
assert.False(t, IsStringValidObjectID(nil, strings.Repeat("a", 65), 4))
assert.False(t, IsStringValidObjectID(nil, strings.Repeat("a", 39)))
assert.True(t, IsStringValidObjectID(nil, strings.Repeat("a", 40)))
assert.False(t, IsStringValidObjectID(nil, strings.Repeat("a", 41)))
assert.True(t, IsStringValidObjectID(nil, strings.Repeat("a", 64)))
assert.False(t, IsStringValidObjectID(nil, strings.Repeat("a", 65)))
assert.False(t, IsStringValidObjectID(Sha1ObjectFormat, strings.Repeat("a", 39)))
assert.True(t, IsStringValidObjectID(Sha1ObjectFormat, strings.Repeat("a", 40)))
assert.False(t, IsStringValidObjectID(Sha1ObjectFormat, strings.Repeat("a", 41)))
assert.False(t, IsStringValidObjectID(invalidObjectFormat, strings.Repeat("a", 40)))
assert.False(t, IsStringValidObjectID(invalidObjectFormat, strings.Repeat("a", 40), 4))
}
+1 -1
View File
@@ -224,7 +224,7 @@ func (ref RefName) RefType() RefType {
return RefTypeBranch
case ref.IsTag():
return RefTypeTag
case IsStringLikelyCommitID(nil, string(ref), 6):
case IsStringValidObjectID(nil, string(ref), 6):
return RefTypeCommit
}
return ""
+2 -2
View File
@@ -203,8 +203,8 @@ func (repo *Repository) searchCommits(ctx context.Context, id ObjectID, opts Sea
// if there are any keywords (ie not committer:, author:, time:)
// then let's iterate over them
for _, v := range opts.Keywords {
// ignore anything not matching a valid sha pattern
if id.Type().IsValid(v) {
// ignore anything not matching a valid sha pattern (TODO: the legacy min-length is pretty small, is it right or fine?)
if IsStringValidObjectID(id.Type(), v, 4) {
// create new git log command with 1 commit limit
hashCmd := gitcmd.NewCommand("log", "-1", prettyLogFormat)
// add previous arguments except for --grep and --all
+1 -2
View File
@@ -13,7 +13,6 @@ import (
"gitea.dev/modules/git/gitcmd"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/hash"
"github.com/go-git/go-git/v5/plumbing/object"
)
@@ -48,7 +47,7 @@ func (repo *Repository) ConvertToGitID(ctx context.Context, commitID string) (Ob
if err != nil {
return nil, err
}
if len(commitID) == hash.HexSize && objectFormat.IsValid(commitID) {
if IsStringValidObjectID(objectFormat, commitID) {
ID, err := NewIDFromString(commitID)
if err == nil {
return ID, nil
+1 -1
View File
@@ -116,7 +116,7 @@ func (repo *Repository) ConvertToGitID(ctx context.Context, ref string) (ObjectI
if err != nil {
return nil, err
}
if len(ref) == objectFormat.FullLength() && objectFormat.IsValid(ref) {
if IsStringValidObjectID(objectFormat, ref) {
id, err := NewIDFromString(ref)
if err == nil {
return id, nil