Currently only SHA1 repositories are supported by Gitea. This adds support for alternate SHA256 with the additional aim of easier support for additional hash types in the future. Fixes: #13794 Limited by: https://github.com/go-git/go-git/issues/899 Depend on: #28138 <img width="776" alt="图片" src="https://github.com/go-gitea/gitea/assets/81045/5448c9a7-608e-4341-a149-5dd0069c9447"> --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: 6543 <6543@obermui.de>tags/v1.22.0-rc0
@@ -37,7 +37,7 @@ type CommitStatus struct { | |||
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"` | |||
TargetURL string `xorm:"TEXT"` | |||
Description string `xorm:"TEXT"` | |||
ContextHash string `xorm:"char(40) index"` | |||
ContextHash string `xorm:"VARCHAR(64) index"` | |||
Context string `xorm:"TEXT"` | |||
Creator *user_model.User `xorm:"-"` | |||
CreatorID int64 |
@@ -270,7 +270,7 @@ type Comment struct { | |||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | |||
// Reference issue in commit message | |||
CommitSHA string `xorm:"VARCHAR(40)"` | |||
CommitSHA string `xorm:"VARCHAR(64)"` | |||
Attachments []*repo_model.Attachment `xorm:"-"` | |||
Reactions ReactionList `xorm:"-"` |
@@ -171,11 +171,11 @@ type PullRequest struct { | |||
HeadBranch string | |||
HeadCommitID string `xorm:"-"` | |||
BaseBranch string | |||
MergeBase string `xorm:"VARCHAR(40)"` | |||
MergeBase string `xorm:"VARCHAR(64)"` | |||
AllowMaintainerEdit bool `xorm:"NOT NULL DEFAULT false"` | |||
HasMerged bool `xorm:"INDEX"` | |||
MergedCommitID string `xorm:"VARCHAR(40)"` | |||
MergedCommitID string `xorm:"VARCHAR(64)"` | |||
MergerID int64 `xorm:"INDEX"` | |||
Merger *user_model.User `xorm:"-"` | |||
MergedUnix timeutil.TimeStamp `xorm:"updated INDEX"` |
@@ -116,7 +116,7 @@ type Review struct { | |||
Content string `xorm:"TEXT"` | |||
// Official is a review made by an assigned approver (counts towards approval) | |||
Official bool `xorm:"NOT NULL DEFAULT false"` | |||
CommitID string `xorm:"VARCHAR(40)"` | |||
CommitID string `xorm:"VARCHAR(64)"` | |||
Stale bool `xorm:"NOT NULL DEFAULT false"` | |||
Dismissed bool `xorm:"NOT NULL DEFAULT false"` | |||
@@ -0,0 +1,11 @@ | |||
# type Repository struct { | |||
# ID int64 `xorm:"pk autoincr"` | |||
# } | |||
- | |||
id: 1 | |||
- | |||
id: 2 | |||
- | |||
id: 3 | |||
- | |||
id: 10 |
@@ -556,6 +556,8 @@ var migrations = []Migration{ | |||
NewMigration("Add ignore stale approval column on branch table", v1_22.AddIgnoreStaleApprovalsColumnToProtectedBranchTable), | |||
// v285 -> v286 | |||
NewMigration("Add PreviousDuration to ActionRun", v1_22.AddPreviousDurationToActionRun), | |||
// v286 -> v287 | |||
NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256), | |||
} | |||
// GetCurrentDBVersion returns the current db version |
@@ -0,0 +1,104 @@ | |||
// Copyright 2023 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package v1_22 //nolint | |||
import ( | |||
"errors" | |||
"fmt" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/setting" | |||
"xorm.io/xorm" | |||
) | |||
func expandHashReferencesToSha256(x *xorm.Engine) error { | |||
alteredTables := [][2]string{ | |||
{"commit_status", "context_hash"}, | |||
{"comment", "commit_sha"}, | |||
{"pull_request", "merge_base"}, | |||
{"pull_request", "merged_commit_id"}, | |||
{"review", "commit_id"}, | |||
{"review_state", "commit_sha"}, | |||
{"repo_archiver", "commit_id"}, | |||
{"release", "sha1"}, | |||
{"repo_indexer_status", "commit_sha"}, | |||
} | |||
db := x.NewSession() | |||
defer db.Close() | |||
if err := db.Begin(); err != nil { | |||
return err | |||
} | |||
if !setting.Database.Type.IsSQLite3() { | |||
if setting.Database.Type.IsMSSQL() { | |||
// drop indexes that need to be re-created afterwards | |||
droppedIndexes := []string{ | |||
"DROP INDEX commit_status.IDX_commit_status_context_hash", | |||
"DROP INDEX review_state.UQE_review_state_pull_commit_user", | |||
"DROP INDEX repo_archiver.UQE_repo_archiver_s", | |||
} | |||
for _, s := range droppedIndexes { | |||
_, err := db.Exec(s) | |||
if err != nil { | |||
return errors.New(s + " " + err.Error()) | |||
} | |||
} | |||
} | |||
for _, alts := range alteredTables { | |||
var err error | |||
if setting.Database.Type.IsMySQL() { | |||
_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` MODIFY COLUMN `%s` VARCHAR(64)", alts[0], alts[1])) | |||
} else if setting.Database.Type.IsMSSQL() { | |||
_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` VARCHAR(64)", alts[0], alts[1])) | |||
} else { | |||
_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` TYPE VARCHAR(64)", alts[0], alts[1])) | |||
} | |||
if err != nil { | |||
return fmt.Errorf("alter column '%s' of table '%s' failed: %w", alts[1], alts[0], err) | |||
} | |||
} | |||
if setting.Database.Type.IsMSSQL() { | |||
recreateIndexes := []string{ | |||
"CREATE INDEX IDX_commit_status_context_hash ON commit_status(context_hash)", | |||
"CREATE UNIQUE INDEX UQE_review_state_pull_commit_user ON review_state(user_id, pull_id, commit_sha)", | |||
"CREATE UNIQUE INDEX UQE_repo_archiver_s ON repo_archiver(repo_id, type, commit_id)", | |||
} | |||
for _, s := range recreateIndexes { | |||
_, err := db.Exec(s) | |||
if err != nil { | |||
return errors.New(s + " " + err.Error()) | |||
} | |||
} | |||
} | |||
} | |||
log.Debug("Updated database tables to hold SHA256 git hash references") | |||
return db.Commit() | |||
} | |||
func addObjectFormatNameToRepository(x *xorm.Engine) error { | |||
type Repository struct { | |||
ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"` | |||
} | |||
if err := x.Sync(new(Repository)); err != nil { | |||
return err | |||
} | |||
// Here to catch weird edge-cases where column constraints above are | |||
// not applied by the DB backend | |||
_, err := x.Exec("UPDATE repository set object_format_name = 'sha1' WHERE object_format_name = '' or object_format_name IS NULL") | |||
return err | |||
} | |||
func AdjustDBForSha256(x *xorm.Engine) error { | |||
if err := expandHashReferencesToSha256(x); err != nil { | |||
return err | |||
} | |||
return addObjectFormatNameToRepository(x) | |||
} |
@@ -0,0 +1,62 @@ | |||
// Copyright 2023 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package v1_22 //nolint | |||
import ( | |||
"testing" | |||
"code.gitea.io/gitea/models/migrations/base" | |||
"github.com/stretchr/testify/assert" | |||
"xorm.io/xorm" | |||
) | |||
func PrepareOldRepository(t *testing.T) (*xorm.Engine, func()) { | |||
type Repository struct { // old struct | |||
ID int64 `xorm:"pk autoincr"` | |||
} | |||
// Prepare and load the testing database | |||
return base.PrepareTestEnv(t, 0, new(Repository)) | |||
} | |||
func Test_RepositoryFormat(t *testing.T) { | |||
x, deferable := PrepareOldRepository(t) | |||
defer deferable() | |||
type Repository struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
ObjectFormatName string `xorg:"not null default('sha1')"` | |||
} | |||
repo := new(Repository) | |||
// check we have some records to migrate | |||
count, err := x.Count(new(Repository)) | |||
assert.NoError(t, err) | |||
assert.EqualValues(t, 4, count) | |||
assert.NoError(t, AdjustDBForSha256(x)) | |||
repo.ID = 20 | |||
repo.ObjectFormatName = "sha256" | |||
_, err = x.Insert(repo) | |||
assert.NoError(t, err) | |||
count, err = x.Count(new(Repository)) | |||
assert.NoError(t, err) | |||
assert.EqualValues(t, 5, count) | |||
repo = new(Repository) | |||
ok, err := x.ID(2).Get(repo) | |||
assert.NoError(t, err) | |||
assert.EqualValues(t, true, ok) | |||
assert.EqualValues(t, "sha1", repo.ObjectFormatName) | |||
repo = new(Repository) | |||
ok, err = x.ID(20).Get(repo) | |||
assert.NoError(t, err) | |||
assert.EqualValues(t, true, ok) | |||
assert.EqualValues(t, "sha256", repo.ObjectFormatName) | |||
} |
@@ -39,7 +39,7 @@ type ReviewState struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
UserID int64 `xorm:"NOT NULL UNIQUE(pull_commit_user)"` | |||
PullID int64 `xorm:"NOT NULL INDEX UNIQUE(pull_commit_user) DEFAULT 0"` // Which PR was the review on? | |||
CommitSHA string `xorm:"NOT NULL VARCHAR(40) UNIQUE(pull_commit_user)"` // Which commit was the head commit for the review? | |||
CommitSHA string `xorm:"NOT NULL VARCHAR(64) UNIQUE(pull_commit_user)"` // Which commit was the head commit for the review? | |||
UpdatedFiles map[string]ViewedState `xorm:"NOT NULL LONGTEXT JSON"` // Stores for each of the changed files of a PR whether they have been viewed, changed since last viewed, or not viewed | |||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"` // Is an accurate indicator of the order of commits as we do not expect it to be possible to make reviews on previous commits | |||
} |
@@ -33,7 +33,7 @@ type RepoArchiver struct { //revive:disable-line:exported | |||
RepoID int64 `xorm:"index unique(s)"` | |||
Type git.ArchiveType `xorm:"unique(s)"` | |||
Status ArchiverStatus | |||
CommitID string `xorm:"VARCHAR(40) unique(s)"` | |||
CommitID string `xorm:"VARCHAR(64) unique(s)"` | |||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL created"` | |||
} | |||
@@ -75,7 +75,7 @@ type Release struct { | |||
Target string | |||
TargetBehind string `xorm:"-"` // to handle non-existing or empty target | |||
Title string | |||
Sha1 string `xorm:"VARCHAR(40)"` | |||
Sha1 string `xorm:"VARCHAR(64)"` | |||
NumCommits int64 | |||
NumCommitsBehind int64 `xorm:"-"` | |||
Note string `xorm:"TEXT"` |
@@ -180,7 +180,7 @@ type Repository struct { | |||
IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"` | |||
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"` | |||
Topics []string `xorm:"TEXT JSON"` | |||
ObjectFormatName string `xorm:"-"` | |||
ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"` | |||
TrustModel TrustModelType | |||
@@ -276,10 +276,6 @@ func (repo *Repository) AfterLoad() { | |||
repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones | |||
repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects | |||
repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns | |||
// this is a temporary behaviour to support old repos, next step is to store the object format in the database | |||
// and read from database so this line could be removed. To not depend on git module, we use a constant variable here | |||
repo.ObjectFormatName = "sha1" | |||
} | |||
// LoadAttributes loads attributes of the repository. |
@@ -27,7 +27,7 @@ const ( | |||
type RepoIndexerStatus struct { //revive:disable-line:exported | |||
ID int64 `xorm:"pk autoincr"` | |||
RepoID int64 `xorm:"INDEX(s)"` | |||
CommitSha string `xorm:"VARCHAR(40)"` | |||
CommitSha string `xorm:"VARCHAR(64)"` | |||
IndexerType RepoIndexerType `xorm:"INDEX(s) NOT NULL DEFAULT 0"` | |||
} | |||
@@ -0,0 +1,144 @@ | |||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package git | |||
import ( | |||
"context" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestReadingBlameOutputSha256(t *testing.T) { | |||
ctx, cancel := context.WithCancel(context.Background()) | |||
defer cancel() | |||
t.Run("Without .git-blame-ignore-revs", func(t *testing.T) { | |||
repo, err := OpenRepository(ctx, "./tests/repos/repo5_pulls_sha256") | |||
assert.NoError(t, err) | |||
defer repo.Close() | |||
commit, err := repo.GetCommit("0b69b7bb649b5d46e14cabb6468685e5dd721290acc7ffe604d37cde57927345") | |||
assert.NoError(t, err) | |||
parts := []*BlamePart{ | |||
{ | |||
Sha: "1e35a51dc00fd7de730344c07061acfe80e8117e075ac979b6a29a3a045190ca", | |||
Lines: []string{ | |||
"# test_repo", | |||
"Test repository for testing migration from github to gitea", | |||
}, | |||
}, | |||
{ | |||
Sha: "0b69b7bb649b5d46e14cabb6468685e5dd721290acc7ffe604d37cde57927345", | |||
Lines: []string{"", "Do not make any changes to this repo it is used for unit testing"}, | |||
PreviousSha: "1e35a51dc00fd7de730344c07061acfe80e8117e075ac979b6a29a3a045190ca", | |||
PreviousPath: "README.md", | |||
}, | |||
} | |||
for _, bypass := range []bool{false, true} { | |||
blameReader, err := CreateBlameReader(ctx, Sha256ObjectFormat, "./tests/repos/repo5_pulls_sha256", commit, "README.md", bypass) | |||
assert.NoError(t, err) | |||
assert.NotNil(t, blameReader) | |||
defer blameReader.Close() | |||
assert.False(t, blameReader.UsesIgnoreRevs()) | |||
for _, part := range parts { | |||
actualPart, err := blameReader.NextPart() | |||
assert.NoError(t, err) | |||
assert.Equal(t, part, actualPart) | |||
} | |||
// make sure all parts have been read | |||
actualPart, err := blameReader.NextPart() | |||
assert.Nil(t, actualPart) | |||
assert.NoError(t, err) | |||
} | |||
}) | |||
t.Run("With .git-blame-ignore-revs", func(t *testing.T) { | |||
repo, err := OpenRepository(ctx, "./tests/repos/repo6_blame_sha256") | |||
assert.NoError(t, err) | |||
defer repo.Close() | |||
full := []*BlamePart{ | |||
{ | |||
Sha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc", | |||
Lines: []string{"line", "line"}, | |||
}, | |||
{ | |||
Sha: "9347b0198cd1f25017579b79d0938fa89dba34ad2514f0dd92f6bc975ed1a2fe", | |||
Lines: []string{"changed line"}, | |||
PreviousSha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc", | |||
PreviousPath: "blame.txt", | |||
}, | |||
{ | |||
Sha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc", | |||
Lines: []string{"line", "line", ""}, | |||
}, | |||
} | |||
cases := []struct { | |||
CommitID string | |||
UsesIgnoreRevs bool | |||
Bypass bool | |||
Parts []*BlamePart | |||
}{ | |||
{ | |||
CommitID: "e2f5660e15159082902960af0ed74fc144921d2b0c80e069361853b3ece29ba3", | |||
UsesIgnoreRevs: true, | |||
Bypass: false, | |||
Parts: []*BlamePart{ | |||
{ | |||
Sha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc", | |||
Lines: []string{"line", "line", "changed line", "line", "line", ""}, | |||
}, | |||
}, | |||
}, | |||
{ | |||
CommitID: "e2f5660e15159082902960af0ed74fc144921d2b0c80e069361853b3ece29ba3", | |||
UsesIgnoreRevs: false, | |||
Bypass: true, | |||
Parts: full, | |||
}, | |||
{ | |||
CommitID: "9347b0198cd1f25017579b79d0938fa89dba34ad2514f0dd92f6bc975ed1a2fe", | |||
UsesIgnoreRevs: false, | |||
Bypass: false, | |||
Parts: full, | |||
}, | |||
{ | |||
CommitID: "9347b0198cd1f25017579b79d0938fa89dba34ad2514f0dd92f6bc975ed1a2fe", | |||
UsesIgnoreRevs: false, | |||
Bypass: false, | |||
Parts: full, | |||
}, | |||
} | |||
for _, c := range cases { | |||
commit, err := repo.GetCommit(c.CommitID) | |||
assert.NoError(t, err) | |||
blameReader, err := CreateBlameReader(ctx, repo.objectFormat, "./tests/repos/repo6_blame_sha256", commit, "blame.txt", c.Bypass) | |||
assert.NoError(t, err) | |||
assert.NotNil(t, blameReader) | |||
defer blameReader.Close() | |||
assert.Equal(t, c.UsesIgnoreRevs, blameReader.UsesIgnoreRevs()) | |||
for _, part := range c.Parts { | |||
actualPart, err := blameReader.NextPart() | |||
assert.NoError(t, err) | |||
assert.Equal(t, part, actualPart) | |||
} | |||
// make sure all parts have been read | |||
actualPart, err := blameReader.NextPart() | |||
assert.Nil(t, actualPart) | |||
assert.NoError(t, err) | |||
} | |||
}) | |||
} |
@@ -85,6 +85,8 @@ readLoop: | |||
commit.Committer.Decode(data) | |||
_, _ = payloadSB.Write(line) | |||
case "gpgsig": | |||
fallthrough | |||
case "gpgsig-sha256": // FIXME: no intertop, so only 1 exists at present. | |||
_, _ = signatureSB.Write(data) | |||
_ = signatureSB.WriteByte('\n') | |||
pgpsig = true |
@@ -0,0 +1,195 @@ | |||
// Copyright 2023 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
//go:build !gogit | |||
package git | |||
import ( | |||
"path/filepath" | |||
"strings" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestCommitsCountSha256(t *testing.T) { | |||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") | |||
commitsCount, err := CommitsCount(DefaultContext, | |||
CommitsCountOptions{ | |||
RepoPath: bareRepo1Path, | |||
Revision: []string{"f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc"}, | |||
}) | |||
assert.NoError(t, err) | |||
assert.Equal(t, int64(3), commitsCount) | |||
} | |||
func TestCommitsCountWithoutBaseSha256(t *testing.T) { | |||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") | |||
commitsCount, err := CommitsCount(DefaultContext, | |||
CommitsCountOptions{ | |||
RepoPath: bareRepo1Path, | |||
Not: "main", | |||
Revision: []string{"branch1"}, | |||
}) | |||
assert.NoError(t, err) | |||
assert.Equal(t, int64(2), commitsCount) | |||
} | |||
func TestGetFullCommitIDSha256(t *testing.T) { | |||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") | |||
id, err := GetFullCommitID(DefaultContext, bareRepo1Path, "f004f4") | |||
assert.NoError(t, err) | |||
assert.Equal(t, "f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc", id) | |||
} | |||
func TestGetFullCommitIDErrorSha256(t *testing.T) { | |||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") | |||
id, err := GetFullCommitID(DefaultContext, bareRepo1Path, "unknown") | |||
assert.Empty(t, id) | |||
if assert.Error(t, err) { | |||
assert.EqualError(t, err, "object does not exist [id: unknown, rel_path: ]") | |||
} | |||
} | |||
func TestCommitFromReaderSha256(t *testing.T) { | |||
commitString := `9433b2a62b964c17a4485ae180f45f595d3e69d31b786087775e28c6b6399df0 commit 1114 | |||
tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e | |||
parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8 | |||
author Adam Majer <amajer@suse.de> 1698676906 +0100 | |||
committer Adam Majer <amajer@suse.de> 1698676906 +0100 | |||
gpgsig-sha256 -----BEGIN PGP SIGNATURE----- | |||
` + " " + ` | |||
iQIrBAABCgAtFiEES+fB08xlgTrzSdQvhkUIsBsmec8FAmU/wKoPHGFtYWplckBz | |||
dXNlLmRlAAoJEIZFCLAbJnnP4s4PQIJATa++WPzR6/H4etT7bsOGoMyguEJYyWOd | |||
aTybplzT7QAL7h2to0QszGabtzMJPIA39xSFZNYNN30voK5YyyYibXluPKgjemfK | |||
WNXwF+gkwgZI38gSvKf+vlqI+EYyIFe19wOhiju0m8SIlB5NEPiWHa17q2mqmqqx | |||
1FWa2JdqLPYjAtSLFXeSZegrY5V1FxdemyMUONkg8YO9OSIMZiE0GsnnOXQ3xcT4 | |||
JTCnmlUxIKw689UiEY80JopUIq+Wl7+qq9507IYYSUCyB6JazL42AKMzVCbD+qBP | |||
oOzh/hafYgk9H9qCQXaLbmvs17zXRpicig1bAzqgAy1FDelvpERyRTydEajSLIG6 | |||
U1cRCkgXCZ0NfsYNPPmBa8b3+rnstypXYTbyMwTln7FfUAaGo6o9JYiPMkzxlmsy | |||
zfp/tcaY8+LlBL9aOJjtv+a0p+HrpCGd6CCa4ARfphTLq8QRSSh8uzlB9N+6HnRI | |||
VAEUo6ecdDxSpyt2naeg9pKus/BRi7P6g4B1hkk/zZstUX/QP4IQuAJbXjkvsC+X | |||
HKRr3NlRM/DygzTyj0gN74uoa0goCIbyAQhiT42nm0cuhM7uN/W0ayrlZjGF1cbR | |||
8NCJUL2Nwj0ywKIavC99Ipkb8AsFwpVT6U6effs6 | |||
=xybZ | |||
-----END PGP SIGNATURE----- | |||
signed commit` | |||
sha := &Sha256Hash{ | |||
0x94, 0x33, 0xb2, 0xa6, 0x2b, 0x96, 0x4c, 0x17, 0xa4, 0x48, 0x5a, 0xe1, 0x80, 0xf4, 0x5f, 0x59, | |||
0x5d, 0x3e, 0x69, 0xd3, 0x1b, 0x78, 0x60, 0x87, 0x77, 0x5e, 0x28, 0xc6, 0xb6, 0x39, 0x9d, 0xf0, | |||
} | |||
gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare_sha256")) | |||
assert.NoError(t, err) | |||
assert.NotNil(t, gitRepo) | |||
defer gitRepo.Close() | |||
commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString)) | |||
assert.NoError(t, err) | |||
if !assert.NotNil(t, commitFromReader) { | |||
return | |||
} | |||
assert.EqualValues(t, sha, commitFromReader.ID) | |||
assert.EqualValues(t, `-----BEGIN PGP SIGNATURE----- | |||
iQIrBAABCgAtFiEES+fB08xlgTrzSdQvhkUIsBsmec8FAmU/wKoPHGFtYWplckBz | |||
dXNlLmRlAAoJEIZFCLAbJnnP4s4PQIJATa++WPzR6/H4etT7bsOGoMyguEJYyWOd | |||
aTybplzT7QAL7h2to0QszGabtzMJPIA39xSFZNYNN30voK5YyyYibXluPKgjemfK | |||
WNXwF+gkwgZI38gSvKf+vlqI+EYyIFe19wOhiju0m8SIlB5NEPiWHa17q2mqmqqx | |||
1FWa2JdqLPYjAtSLFXeSZegrY5V1FxdemyMUONkg8YO9OSIMZiE0GsnnOXQ3xcT4 | |||
JTCnmlUxIKw689UiEY80JopUIq+Wl7+qq9507IYYSUCyB6JazL42AKMzVCbD+qBP | |||
oOzh/hafYgk9H9qCQXaLbmvs17zXRpicig1bAzqgAy1FDelvpERyRTydEajSLIG6 | |||
U1cRCkgXCZ0NfsYNPPmBa8b3+rnstypXYTbyMwTln7FfUAaGo6o9JYiPMkzxlmsy | |||
zfp/tcaY8+LlBL9aOJjtv+a0p+HrpCGd6CCa4ARfphTLq8QRSSh8uzlB9N+6HnRI | |||
VAEUo6ecdDxSpyt2naeg9pKus/BRi7P6g4B1hkk/zZstUX/QP4IQuAJbXjkvsC+X | |||
HKRr3NlRM/DygzTyj0gN74uoa0goCIbyAQhiT42nm0cuhM7uN/W0ayrlZjGF1cbR | |||
8NCJUL2Nwj0ywKIavC99Ipkb8AsFwpVT6U6effs6 | |||
=xybZ | |||
-----END PGP SIGNATURE----- | |||
`, commitFromReader.Signature.Signature) | |||
assert.EqualValues(t, `tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e | |||
parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8 | |||
author Adam Majer <amajer@suse.de> 1698676906 +0100 | |||
committer Adam Majer <amajer@suse.de> 1698676906 +0100 | |||
signed commit`, commitFromReader.Signature.Payload) | |||
assert.EqualValues(t, "Adam Majer <amajer@suse.de>", commitFromReader.Author.String()) | |||
commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n")) | |||
assert.NoError(t, err) | |||
commitFromReader.CommitMessage += "\n\n" | |||
commitFromReader.Signature.Payload += "\n\n" | |||
assert.EqualValues(t, commitFromReader, commitFromReader2) | |||
} | |||
func TestHasPreviousCommitSha256(t *testing.T) { | |||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") | |||
repo, err := openRepositoryWithDefaultContext(bareRepo1Path) | |||
assert.NoError(t, err) | |||
defer repo.Close() | |||
commit, err := repo.GetCommit("f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc") | |||
assert.NoError(t, err) | |||
parentSHA := MustIDFromString("b0ec7af4547047f12d5093e37ef8f1b3b5415ed8ee17894d43a34d7d34212e9c") | |||
notParentSHA := MustIDFromString("42e334efd04cd36eea6da0599913333c26116e1a537ca76e5b6e4af4dda00236") | |||
assert.Equal(t, repo.objectFormat, parentSHA.Type()) | |||
assert.Equal(t, repo.objectFormat.Name(), "sha256") | |||
haz, err := commit.HasPreviousCommit(parentSHA) | |||
assert.NoError(t, err) | |||
assert.True(t, haz) | |||
hazNot, err := commit.HasPreviousCommit(notParentSHA) | |||
assert.NoError(t, err) | |||
assert.False(t, hazNot) | |||
selfNot, err := commit.HasPreviousCommit(commit.ID) | |||
assert.NoError(t, err) | |||
assert.False(t, selfNot) | |||
} | |||
func TestGetCommitFileStatusMergesSha256(t *testing.T) { | |||
bareRepo1Path := filepath.Join(testReposDir, "repo6_merge_sha256") | |||
commitFileStatus, err := GetCommitFileStatus(DefaultContext, bareRepo1Path, "d2e5609f630dd8db500f5298d05d16def282412e3e66ed68cc7d0833b29129a1") | |||
assert.NoError(t, err) | |||
expected := CommitFileStatus{ | |||
[]string{ | |||
"add_file.txt", | |||
}, | |||
[]string{}, | |||
[]string{ | |||
"to_modify.txt", | |||
}, | |||
} | |||
assert.Equal(t, expected.Added, commitFileStatus.Added) | |||
assert.Equal(t, expected.Removed, commitFileStatus.Removed) | |||
assert.Equal(t, expected.Modified, commitFileStatus.Modified) | |||
expected = CommitFileStatus{ | |||
[]string{}, | |||
[]string{ | |||
"to_remove.txt", | |||
}, | |||
[]string{}, | |||
} | |||
commitFileStatus, err = GetCommitFileStatus(DefaultContext, bareRepo1Path, "da1ded40dc8e5b7c564171f4bf2fc8370487decfb1cb6a99ef28f3ed73d09172") | |||
assert.NoError(t, err) | |||
assert.Equal(t, expected.Added, commitFileStatus.Added) | |||
assert.Equal(t, expected.Removed, commitFileStatus.Removed) | |||
assert.Equal(t, expected.Modified, commitFileStatus.Modified) | |||
} |
@@ -185,7 +185,13 @@ func InitFull(ctx context.Context) (err error) { | |||
globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=") | |||
} | |||
SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil | |||
SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil | |||
SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit | |||
if SupportHashSha256 { | |||
SupportedObjectFormats = append(SupportedObjectFormats, Sha256ObjectFormat) | |||
} else { | |||
log.Warn("sha256 hash support is disabled - requires Git >= 2.42. Gogit is currently unsupported") | |||
} | |||
if setting.LFS.StartServer { | |||
if CheckGitVersionAtLeast("2.1.2") != nil { | |||
return errors.New("LFS server support requires Git >= 2.1.2") |
@@ -5,6 +5,7 @@ package git | |||
import ( | |||
"crypto/sha1" | |||
"crypto/sha256" | |||
"regexp" | |||
"strconv" | |||
) | |||
@@ -12,6 +13,9 @@ import ( | |||
// sha1Pattern can be used to determine if a string is an valid sha | |||
var sha1Pattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`) | |||
// sha256Pattern can be used to determine if a string is an valid sha | |||
var sha256Pattern = regexp.MustCompile(`^[0-9a-f]{4,64}$`) | |||
type ObjectFormat interface { | |||
// Name returns the name of the object format | |||
Name() string | |||
@@ -29,11 +33,12 @@ type ObjectFormat interface { | |||
ComputeHash(t ObjectType, content []byte) ObjectID | |||
} | |||
/* SHA1 Type */ | |||
type Sha1ObjectFormatImpl struct{} | |||
var ( | |||
emptyObjectID = &Sha1Hash{} | |||
emptyTree = &Sha1Hash{ | |||
emptySha1ObjectID = &Sha1Hash{} | |||
emptySha1Tree = &Sha1Hash{ | |||
0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60, | |||
0xe5, 0x4b, 0xf8, 0xd6, 0x92, 0x88, 0xfb, 0xee, 0x49, 0x04, | |||
} | |||
@@ -41,11 +46,11 @@ var ( | |||
func (Sha1ObjectFormatImpl) Name() string { return "sha1" } | |||
func (Sha1ObjectFormatImpl) EmptyObjectID() ObjectID { | |||
return emptyObjectID | |||
return emptySha1ObjectID | |||
} | |||
func (Sha1ObjectFormatImpl) EmptyTree() ObjectID { | |||
return emptyTree | |||
return emptySha1Tree | |||
} | |||
func (Sha1ObjectFormatImpl) FullLength() int { return 40 } | |||
func (Sha1ObjectFormatImpl) IsValid(input string) bool { | |||
@@ -72,11 +77,59 @@ func (h Sha1ObjectFormatImpl) ComputeHash(t ObjectType, content []byte) ObjectID | |||
return &sha1 | |||
} | |||
var Sha1ObjectFormat ObjectFormat = Sha1ObjectFormatImpl{} | |||
/* SHA256 Type */ | |||
type Sha256ObjectFormatImpl struct{} | |||
var ( | |||
emptySha256ObjectID = &Sha256Hash{} | |||
emptySha256Tree = &Sha256Hash{ | |||
0x6e, 0xf1, 0x9b, 0x41, 0x22, 0x5c, 0x53, 0x69, 0xf1, 0xc1, | |||
0x04, 0xd4, 0x5d, 0x8d, 0x85, 0xef, 0xa9, 0xb0, 0x57, 0xb5, | |||
0x3b, 0x14, 0xb4, 0xb9, 0xb9, 0x39, 0xdd, 0x74, 0xde, 0xcc, | |||
0x53, 0x21, | |||
} | |||
) | |||
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) MustID(b []byte) ObjectID { | |||
var id Sha256Hash | |||
copy(id[0:32], b) | |||
return &id | |||
} | |||
// ComputeHash compute the hash for a given ObjectType and content | |||
func (h Sha256ObjectFormatImpl) ComputeHash(t ObjectType, content []byte) ObjectID { | |||
hasher := sha256.New() | |||
_, _ = hasher.Write(t.Bytes()) | |||
_, _ = hasher.Write([]byte(" ")) | |||
_, _ = hasher.Write([]byte(strconv.FormatInt(int64(len(content)), 10))) | |||
_, _ = hasher.Write([]byte{0}) | |||
// HashSum generates a SHA256 for the provided hash | |||
var sha256 Sha1Hash | |||
copy(sha256[:], hasher.Sum(nil)) | |||
return &sha256 | |||
} | |||
var ( | |||
Sha1ObjectFormat ObjectFormat = Sha1ObjectFormatImpl{} | |||
Sha256ObjectFormat ObjectFormat = Sha256ObjectFormatImpl{} | |||
) | |||
var SupportedObjectFormats = []ObjectFormat{ | |||
Sha1ObjectFormat, | |||
// TODO: add sha256 | |||
} | |||
func ObjectFormatFromName(name string) ObjectFormat { |
@@ -16,6 +16,7 @@ type ObjectID interface { | |||
Type() ObjectFormat | |||
} | |||
/* SHA1 */ | |||
type Sha1Hash [20]byte | |||
func (h *Sha1Hash) String() string { | |||
@@ -39,6 +40,21 @@ func MustIDFromString(hexHash string) ObjectID { | |||
return id | |||
} | |||
/* SHA256 */ | |||
type Sha256Hash [32]byte | |||
func (h *Sha256Hash) String() string { | |||
return hex.EncodeToString(h[:]) | |||
} | |||
func (h *Sha256Hash) IsZero() bool { | |||
empty := Sha256Hash{} | |||
return bytes.Equal(empty[:], h[:]) | |||
} | |||
func (h *Sha256Hash) RawValue() []byte { return h[:] } | |||
func (*Sha256Hash) Type() ObjectFormat { return Sha256ObjectFormat } | |||
/* utility */ | |||
func NewIDFromString(hexHash string) (ObjectID, error) { | |||
var theObjectFormat ObjectFormat | |||
for _, objectFormat := range SupportedObjectFormats { |
@@ -13,6 +13,8 @@ func ParseGogitHash(h plumbing.Hash) ObjectID { | |||
switch hash.Size { | |||
case 20: | |||
return Sha1ObjectFormat.MustID(h[:]) | |||
case 32: | |||
return Sha256ObjectFormat.MustID(h[:]) | |||
} | |||
return nil |
@@ -97,15 +97,12 @@ func InitRepository(ctx context.Context, repoPath string, bare bool, objectForma | |||
} | |||
cmd := NewCommand(ctx, "init") | |||
if SupportHashSha256 { | |||
if objectFormatName == "" { | |||
objectFormatName = Sha1ObjectFormat.Name() | |||
} | |||
if !IsValidObjectFormat(objectFormatName) { | |||
return fmt.Errorf("invalid object format: %s", objectFormatName) | |||
} | |||
cmd.AddOptionValues("--object-format", objectFormatName) | |||
if !IsValidObjectFormat(objectFormatName) { | |||
return fmt.Errorf("invalid object format: %s", objectFormatName) | |||
} | |||
cmd.AddOptionValues("--object-format", objectFormatName) | |||
if bare { | |||
cmd.AddArguments("--bare") | |||
} |
@@ -8,6 +8,8 @@ import ( | |||
"io" | |||
) | |||
var isGogit bool | |||
// contextKey is a value for use with context.WithValue. | |||
type contextKey struct { | |||
name string |
@@ -21,6 +21,10 @@ import ( | |||
"github.com/go-git/go-git/v5/storage/filesystem" | |||
) | |||
func init() { | |||
isGogit = true | |||
} | |||
// Repository represents a Git repository. | |||
type Repository struct { | |||
Path string |
@@ -15,6 +15,10 @@ import ( | |||
"code.gitea.io/gitea/modules/log" | |||
) | |||
func init() { | |||
isGogit = false | |||
} | |||
// Repository represents a Git repository. | |||
type Repository struct { | |||
Path string |
@@ -0,0 +1 @@ | |||
ref: refs/heads/main |
@@ -0,0 +1,6 @@ | |||
[core] | |||
repositoryformatversion = 1 | |||
filemode = true | |||
bare = true | |||
[extensions] | |||
objectformat = sha256 |
@@ -0,0 +1 @@ | |||
Unnamed repository; edit this file 'description' to name the repository. |
@@ -0,0 +1,6 @@ | |||
# git ls-files --others --exclude-from=.git/info/exclude | |||
# Lines that start with '#' are comments. | |||
# For a project mostly in C, the following would be a good set of | |||
# exclude patterns (uncomment them if you want to use them): | |||
# *.[oa] | |||
# *~ |
@@ -0,0 +1,7 @@ | |||
42e334efd04cd36eea6da0599913333c26116e1a537ca76e5b6e4af4dda00236 refs/heads/branch1 | |||
5bc2249e32e0ba40a08879fba2bd4e97a13cb345831549f4bc5649525da8f6cc refs/heads/branch2 | |||
9433b2a62b964c17a4485ae180f45f595d3e69d31b786087775e28c6b6399df0 refs/heads/main | |||
29a82d4fc02e19190fb489cc90d5730ed91970b49f4e39acda2798b3dd4f814e refs/tags/signed-tag | |||
9433b2a62b964c17a4485ae180f45f595d3e69d31b786087775e28c6b6399df0 refs/tags/signed-tag^{} | |||
171822a62559f3aa28a00aa3785dbe915d6a8eb02712682740db44fc8bd2187a refs/tags/test | |||
6aae864a3d1d0d6a5be0cc64028c1e7021e2632b031fd8eb82afc5a283d1c3d1 refs/tags/test^{} |
@@ -0,0 +1,2 @@ | |||
P pack-c01aa121b9c5e345fe0da2f9be78665970b0c38c6b495d5fc034bc7a7b95334b.pack | |||
@@ -0,0 +1,8 @@ | |||
# pack-refs with: peeled fully-peeled sorted | |||
42e334efd04cd36eea6da0599913333c26116e1a537ca76e5b6e4af4dda00236 refs/heads/branch1 | |||
5bc2249e32e0ba40a08879fba2bd4e97a13cb345831549f4bc5649525da8f6cc refs/heads/branch2 | |||
9433b2a62b964c17a4485ae180f45f595d3e69d31b786087775e28c6b6399df0 refs/heads/main | |||
29a82d4fc02e19190fb489cc90d5730ed91970b49f4e39acda2798b3dd4f814e refs/tags/signed-tag | |||
^9433b2a62b964c17a4485ae180f45f595d3e69d31b786087775e28c6b6399df0 | |||
171822a62559f3aa28a00aa3785dbe915d6a8eb02712682740db44fc8bd2187a refs/tags/test | |||
^6aae864a3d1d0d6a5be0cc64028c1e7021e2632b031fd8eb82afc5a283d1c3d1 |
@@ -0,0 +1 @@ | |||
9433b2a62b964c17a4485ae180f45f595d3e69d31b786087775e28c6b6399df0 |
@@ -0,0 +1 @@ | |||
ref: refs/heads/main |
@@ -0,0 +1,6 @@ | |||
[core] | |||
repositoryformatversion = 1 | |||
filemode = true | |||
bare = true | |||
[extensions] | |||
objectformat = sha256 |
@@ -0,0 +1 @@ | |||
Unnamed repository; edit this file 'description' to name the repository. |
@@ -0,0 +1,4 @@ | |||
35ecd0f946c8baeb76fa5a3876f46bf35218655e2304d8505026fa4bfb496a4b refs/heads/main | |||
35ecd0f946c8baeb76fa5a3876f46bf35218655e2304d8505026fa4bfb496a4b refs/heads/main-clone | |||
7f50a4906503378b0bbb7d61bd2ca8d8d8ff4f7a2474980f99402d742ccc9665 refs/heads/test-patch-1 | |||
1e35a51dc00fd7de730344c07061acfe80e8117e075ac979b6a29a3a045190ca refs/tags/v0.9.99 |
@@ -0,0 +1,2 @@ | |||
P pack-bfe8f09d42ef5dd1610bf42641fe145d4a02b788eb26c31022a362312660a29d.pack | |||
@@ -0,0 +1,5 @@ | |||
# pack-refs with: peeled fully-peeled sorted | |||
35ecd0f946c8baeb76fa5a3876f46bf35218655e2304d8505026fa4bfb496a4b refs/heads/main | |||
35ecd0f946c8baeb76fa5a3876f46bf35218655e2304d8505026fa4bfb496a4b refs/heads/main-clone | |||
7f50a4906503378b0bbb7d61bd2ca8d8d8ff4f7a2474980f99402d742ccc9665 refs/heads/test-patch-1 | |||
1e35a51dc00fd7de730344c07061acfe80e8117e075ac979b6a29a3a045190ca refs/tags/v0.9.99 |
@@ -0,0 +1 @@ | |||
35ecd0f946c8baeb76fa5a3876f46bf35218655e2304d8505026fa4bfb496a4b |
@@ -0,0 +1 @@ | |||
ref: refs/heads/main |
@@ -0,0 +1,6 @@ | |||
[core] | |||
repositoryformatversion = 1 | |||
filemode = true | |||
bare = true | |||
[extensions] | |||
objectformat = sha256 |
@@ -0,0 +1 @@ | |||
Unnamed repository; edit this file 'description' to name the repository. |
@@ -0,0 +1,6 @@ | |||
# git ls-files --others --exclude-from=.git/info/exclude | |||
# Lines that start with '#' are comments. | |||
# For a project mostly in C, the following would be a good set of | |||
# exclude patterns (uncomment them if you want to use them): | |||
# *.[oa] | |||
# *~ |
@@ -0,0 +1 @@ | |||
e2f5660e15159082902960af0ed74fc144921d2b0c80e069361853b3ece29ba3 refs/heads/main |
@@ -0,0 +1,2 @@ | |||
P pack-fcb8a221b76025fd8415d3c562b611ac24312a5ffc3d3703d7c5cc906bdaee8e.pack | |||
@@ -0,0 +1,2 @@ | |||
# pack-refs with: peeled fully-peeled sorted | |||
e2f5660e15159082902960af0ed74fc144921d2b0c80e069361853b3ece29ba3 refs/heads/main |
@@ -0,0 +1 @@ | |||
e2f5660e15159082902960af0ed74fc144921d2b0c80e069361853b3ece29ba3 |
@@ -0,0 +1 @@ | |||
ref: refs/heads/main |
@@ -0,0 +1,6 @@ | |||
[core] | |||
repositoryformatversion = 1 | |||
filemode = true | |||
bare = true | |||
[extensions] | |||
objectformat = sha256 |
@@ -0,0 +1 @@ | |||
Unnamed repository; edit this file 'description' to name the repository. |
@@ -0,0 +1,6 @@ | |||
# git ls-files --others --exclude-from=.git/info/exclude | |||
# Lines that start with '#' are comments. | |||
# For a project mostly in C, the following would be a good set of | |||
# exclude patterns (uncomment them if you want to use them): | |||
# *.[oa] | |||
# *~ |
@@ -0,0 +1,4 @@ | |||
d2e5609f630dd8db500f5298d05d16def282412e3e66ed68cc7d0833b29129a1 refs/heads/main | |||
b45258e9823233edea2d40d183742f29630e1e69300479fb4a55eabfe9b1d8bf refs/heads/merge/add_file | |||
ff2b996e2fa366146300e4c9e51ccb6818147b360e46fa1437334f4a690955ce refs/heads/merge/modify_file | |||
da1ded40dc8e5b7c564171f4bf2fc8370487decfb1cb6a99ef28f3ed73d09172 refs/heads/merge/remove_file |
@@ -0,0 +1,3 @@ | |||
P pack-2fff0848f8d8eab8f7902ac91ab6a096c7530f577d5c0a79c63d9ac2b44f7510.pack | |||
P pack-65162b86afdbac3c566696d487e67bb2a4a5501ca1fa3528fad8a9474fba7e50.pack | |||
@@ -0,0 +1,5 @@ | |||
# pack-refs with: peeled fully-peeled sorted | |||
d2e5609f630dd8db500f5298d05d16def282412e3e66ed68cc7d0833b29129a1 refs/heads/main | |||
b45258e9823233edea2d40d183742f29630e1e69300479fb4a55eabfe9b1d8bf refs/heads/merge/add_file | |||
ff2b996e2fa366146300e4c9e51ccb6818147b360e46fa1437334f4a690955ce refs/heads/merge/modify_file | |||
da1ded40dc8e5b7c564171f4bf2fc8370487decfb1cb6a99ef28f3ed73d09172 refs/heads/merge/remove_file |
@@ -0,0 +1 @@ | |||
d2e5609f630dd8db500f5298d05d16def282412e3e66ed68cc7d0833b29129a1 |
@@ -45,19 +45,19 @@ var ( | |||
// valid chars in encoded path and parameter: [-+~_%.a-zA-Z0-9/] | |||
// sha1CurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae | |||
// Although SHA1 hashes are 40 chars long, the regex matches the hash from 7 to 40 chars in length | |||
// hashCurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae | |||
// Although SHA1 hashes are 40 chars long, SHA256 are 64, the regex matches the hash from 7 to 64 chars in length | |||
// so that abbreviated hash links can be used as well. This matches git and GitHub usability. | |||
sha1CurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,40})(?:\s|$|\)|\]|[.,](\s|$))`) | |||
hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,](\s|$))`) | |||
// shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax | |||
shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`) | |||
// anySHA1Pattern splits url containing SHA into parts | |||
anySHA1Pattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40})(/[-+~_%.a-zA-Z0-9/]+)?(#[-+~_%.a-zA-Z0-9]+)?`) | |||
anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~_%.a-zA-Z0-9/]+)?(#[-+~_%.a-zA-Z0-9]+)?`) | |||
// comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash" | |||
comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,40})(\.\.\.?)([0-9a-f]{7,40})?(#[-+~_%.a-zA-Z0-9]+)?`) | |||
comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`) | |||
validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`) | |||
@@ -171,13 +171,13 @@ type processor func(ctx *RenderContext, node *html.Node) | |||
var defaultProcessors = []processor{ | |||
fullIssuePatternProcessor, | |||
comparePatternProcessor, | |||
fullSha1PatternProcessor, | |||
fullHashPatternProcessor, | |||
shortLinkProcessor, | |||
linkProcessor, | |||
mentionProcessor, | |||
issueIndexPatternProcessor, | |||
commitCrossReferencePatternProcessor, | |||
sha1CurrentPatternProcessor, | |||
hashCurrentPatternProcessor, | |||
emailAddressProcessor, | |||
emojiProcessor, | |||
emojiShortCodeProcessor, | |||
@@ -199,12 +199,12 @@ func PostProcess( | |||
var commitMessageProcessors = []processor{ | |||
fullIssuePatternProcessor, | |||
comparePatternProcessor, | |||
fullSha1PatternProcessor, | |||
fullHashPatternProcessor, | |||
linkProcessor, | |||
mentionProcessor, | |||
issueIndexPatternProcessor, | |||
commitCrossReferencePatternProcessor, | |||
sha1CurrentPatternProcessor, | |||
hashCurrentPatternProcessor, | |||
emailAddressProcessor, | |||
emojiProcessor, | |||
emojiShortCodeProcessor, | |||
@@ -231,12 +231,12 @@ func RenderCommitMessage( | |||
var commitMessageSubjectProcessors = []processor{ | |||
fullIssuePatternProcessor, | |||
comparePatternProcessor, | |||
fullSha1PatternProcessor, | |||
fullHashPatternProcessor, | |||
linkProcessor, | |||
mentionProcessor, | |||
issueIndexPatternProcessor, | |||
commitCrossReferencePatternProcessor, | |||
sha1CurrentPatternProcessor, | |||
hashCurrentPatternProcessor, | |||
emojiShortCodeProcessor, | |||
emojiProcessor, | |||
} | |||
@@ -273,7 +273,7 @@ func RenderIssueTitle( | |||
return renderProcessString(ctx, []processor{ | |||
issueIndexPatternProcessor, | |||
commitCrossReferencePatternProcessor, | |||
sha1CurrentPatternProcessor, | |||
hashCurrentPatternProcessor, | |||
emojiShortCodeProcessor, | |||
emojiProcessor, | |||
}, title) | |||
@@ -946,15 +946,15 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { | |||
} | |||
} | |||
// fullSha1PatternProcessor renders SHA containing URLs | |||
func fullSha1PatternProcessor(ctx *RenderContext, node *html.Node) { | |||
// fullHashPatternProcessor renders SHA containing URLs | |||
func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) { | |||
if ctx.Metas == nil { | |||
return | |||
} | |||
next := node.NextSibling | |||
for node != nil && node != next { | |||
m := anySHA1Pattern.FindStringSubmatchIndex(node.Data) | |||
m := anyHashPattern.FindStringSubmatchIndex(node.Data) | |||
if m == nil { | |||
return | |||
} | |||
@@ -1111,9 +1111,9 @@ func emojiProcessor(ctx *RenderContext, node *html.Node) { | |||
} | |||
} | |||
// sha1CurrentPatternProcessor renders SHA1 strings to corresponding links that | |||
// hashCurrentPatternProcessor renders SHA1 strings to corresponding links that | |||
// are assumed to be in the same repository. | |||
func sha1CurrentPatternProcessor(ctx *RenderContext, node *html.Node) { | |||
func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) { | |||
if ctx.Metas == nil || ctx.Metas["user"] == "" || ctx.Metas["repo"] == "" || ctx.Metas["repoPath"] == "" { | |||
return | |||
} | |||
@@ -1124,7 +1124,7 @@ func sha1CurrentPatternProcessor(ctx *RenderContext, node *html.Node) { | |||
ctx.ShaExistCache = make(map[string]bool) | |||
} | |||
for node != nil && node != next && start < len(node.Data) { | |||
m := sha1CurrentPattern.FindStringSubmatchIndex(node.Data[start:]) | |||
m := hashCurrentPattern.FindStringSubmatchIndex(node.Data[start:]) | |||
if m == nil { | |||
return | |||
} |
@@ -390,10 +390,10 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) { | |||
} | |||
for _, testCase := range trueTestCases { | |||
assert.True(t, sha1CurrentPattern.MatchString(testCase)) | |||
assert.True(t, hashCurrentPattern.MatchString(testCase)) | |||
} | |||
for _, testCase := range falseTestCases { | |||
assert.False(t, sha1CurrentPattern.MatchString(testCase)) | |||
assert.False(t, hashCurrentPattern.MatchString(testCase)) | |||
} | |||
} | |||
@@ -427,7 +427,7 @@ func TestRegExp_anySHA1Pattern(t *testing.T) { | |||
} | |||
for k, v := range testCases { | |||
assert.Equal(t, anySHA1Pattern.FindStringSubmatch(k)[1:], v) | |||
assert.Equal(t, anyHashPattern.FindStringSubmatch(k)[1:], v) | |||
} | |||
} | |||
@@ -39,7 +39,7 @@ var ( | |||
crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) | |||
// crossReferenceCommitPattern matches a string that references a commit in a different repository | |||
// e.g. go-gitea/gitea@d8a994ef, go-gitea/gitea@d8a994ef243349f321568f9e36d5c3f444b99cae (7-40 characters) | |||
crossReferenceCommitPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+)@([0-9a-f]{7,40})(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) | |||
crossReferenceCommitPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+)@([0-9a-f]{7,64})(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) | |||
// spaceTrimmedPattern let's find the trailing space | |||
spaceTrimmedPattern = regexp.MustCompile(`(?:.*[0-9a-zA-Z-_])\s`) | |||
// timeLogPattern matches string for time tracking |
@@ -343,7 +343,7 @@ func TestFindRenderizableCommitCrossReference(t *testing.T) { | |||
}, | |||
}, | |||
{ | |||
Input: "go-gitea/gitea@abcd1234abcd1234abcd1234abcd1234abcd12340", // longer than 40 characters | |||
Input: "go-gitea/gitea@abcd1234abcd1234abcd1234abcd1234abcd12341234512345123451234512345", // longer than 64 characters | |||
Expected: nil, | |||
}, | |||
{ |
@@ -105,6 +105,9 @@ type Repository struct { | |||
AvatarURL string `json:"avatar_url"` | |||
Internal bool `json:"internal"` | |||
MirrorInterval string `json:"mirror_interval"` | |||
// ObjectFormatName of the underlying git repository | |||
// enum: sha1,sha256 | |||
ObjectFormatName string `json:"object_format_name"` | |||
// swagger:strfmt date-time | |||
MirrorUpdated time.Time `json:"mirror_updated,omitempty"` | |||
RepoTransfer *RepoTransfer `json:"repo_transfer"` | |||
@@ -139,6 +142,9 @@ type CreateRepoOption struct { | |||
// TrustModel of the repository | |||
// enum: default,collaborator,committer,collaboratorcommitter | |||
TrustModel string `json:"trust_model"` | |||
// ObjectFormatName of the underlying git repository | |||
// enum: sha1,sha256 | |||
ObjectFormatName string `json:"object_format_name" binding:"MaxSize(6)"` | |||
} | |||
// EditRepoOption options when editing a repository's properties |
@@ -41,3 +41,7 @@ func (su *StringUtils) Cut(s, sep string) []any { | |||
func (su *StringUtils) EllipsisString(s string, max int) string { | |||
return base.EllipsisString(s, max) | |||
} | |||
func (su *StringUtils) ToUpper(s string) string { | |||
return strings.ToUpper(s) | |||
} |
@@ -970,6 +970,8 @@ issue_labels_helper = Select an issue label set. | |||
license = License | |||
license_helper = Select a license file. | |||
license_helper_desc = A license governs what others can and can't do with your code. Not sure which one is right for your project? See <a target="_blank" rel="noopener noreferrer" href="%s">Choose a license.</a> | |||
object_format = Object Format | |||
object_format_helper = Object format of the repository. Cannot be changed later. SHA1 is most compatible. | |||
readme = README | |||
readme_helper = Select a README file template. | |||
readme_helper_desc = This is the place where you can write a complete description for your project. | |||
@@ -1038,6 +1040,7 @@ desc.public = Public | |||
desc.template = Template | |||
desc.internal = Internal | |||
desc.archived = Archived | |||
desc.sha256 = SHA256 | |||
template.items = Template Items | |||
template.git_content = Git Content (Default Branch) |
@@ -253,7 +253,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre | |||
DefaultBranch: opt.DefaultBranch, | |||
TrustModel: repo_model.ToTrustModel(opt.TrustModel), | |||
IsTemplate: opt.Template, | |||
ObjectFormatName: git.Sha1ObjectFormat.Name(), | |||
ObjectFormatName: opt.ObjectFormatName, | |||
}) | |||
if err != nil { | |||
if repo_model.IsErrRepoAlreadyExist(err) { |
@@ -36,8 +36,8 @@ func gitHTTPRouters(m *web.Route) { | |||
m.Methods("GET,OPTIONS", "/objects/info/http-alternates", repo.GetTextFile("objects/info/http-alternates")) | |||
m.Methods("GET,OPTIONS", "/objects/info/packs", repo.GetInfoPacks) | |||
m.Methods("GET,OPTIONS", "/objects/info/{file:[^/]*}", repo.GetTextFile("")) | |||
m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", repo.GetLooseObject) | |||
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40}}.pack", repo.GetPackFile) | |||
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40}}.idx", repo.GetIdxFile) | |||
m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject) | |||
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile) | |||
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile) | |||
}, ignSignInAndCsrf, requireSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context_service.UserAssignmentWeb()) | |||
} |
@@ -159,7 +159,6 @@ func Create(ctx *context.Context) { | |||
ctx.Data["private"] = getRepoPrivate(ctx) | |||
ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate | |||
ctx.Data["default_branch"] = setting.Repository.DefaultBranch | |||
ctx.Data["hash_type"] = "sha1" | |||
ctxUser := checkContextUser(ctx, ctx.FormInt64("org")) | |||
if ctx.Written() { | |||
@@ -179,6 +178,8 @@ func Create(ctx *context.Context) { | |||
ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() | |||
ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() | |||
ctx.Data["SupportedObjectFormats"] = git.SupportedObjectFormats | |||
ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat | |||
ctx.HTML(http.StatusOK, tplCreate) | |||
} |
@@ -1235,7 +1235,7 @@ func registerRoutes(m *web.Route) { | |||
Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost) | |||
m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch). | |||
Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost) | |||
m.Combo("/_cherrypick/{sha:([a-f0-9]{7,40})}/*").Get(repo.CherryPick). | |||
m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick). | |||
Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost) | |||
}, repo.MustBeEditable) | |||
m.Group("", func() { | |||
@@ -1377,8 +1377,8 @@ func registerRoutes(m *web.Route) { | |||
m.Combo("/*"). | |||
Get(repo.Wiki). | |||
Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) | |||
m.Get("/commit/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) | |||
m.Get("/commit/{sha:[a-f0-9]{7,40}}.{ext:patch|diff}", repo.RawDiff) | |||
m.Get("/commit/{sha:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) | |||
m.Get("/commit/{sha:[a-f0-9]{7,64}}.{ext:patch|diff}", repo.RawDiff) | |||
}, repo.MustEnableWiki, func(ctx *context.Context) { | |||
ctx.Data["PageIsWiki"] = true | |||
ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink() | |||
@@ -1498,9 +1498,9 @@ func registerRoutes(m *web.Route) { | |||
m.Group("", func() { | |||
m.Get("/graph", repo.Graph) | |||
m.Get("/commit/{sha:([a-f0-9]{7,40})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) | |||
m.Get("/commit/{sha:([a-f0-9]{7,40})$}/load-branches-and-tags", repo.LoadBranchesAndTags) | |||
m.Get("/cherry-pick/{sha:([a-f0-9]{7,40})$}", repo.SetEditorconfigIfExists, repo.CherryPick) | |||
m.Get("/commit/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) | |||
m.Get("/commit/{sha:([a-f0-9]{7,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags) | |||
m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick) | |||
}, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader) | |||
m.Get("/rss/branch/*", context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed) | |||
@@ -1517,7 +1517,7 @@ func registerRoutes(m *web.Route) { | |||
m.Group("", func() { | |||
m.Get("/forks", repo.Forks) | |||
}, context.RepoRef(), reqRepoCodeReader) | |||
m.Get("/commit/{sha:([a-f0-9]{7,40})}.{ext:patch|diff}", repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff) | |||
m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff) | |||
}, ignSignIn, context.RepoAssignment, context.UnitTypes()) | |||
m.Post("/{username}/{reponame}/lastcommit/*", ignSignInAndCsrf, context.RepoAssignment, context.UnitTypes(), context.RepoRefByType(context.RepoRefCommit), reqRepoCodeReader, repo.LastCommit) |
@@ -217,6 +217,10 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt | |||
} | |||
} | |||
if opts.ObjectFormatName == "" { | |||
opts.ObjectFormatName = git.Sha1ObjectFormat.Name() | |||
} | |||
repo := &repo_model.Repository{ | |||
OwnerID: u.ID, | |||
Owner: u, |
@@ -76,17 +76,18 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork | |||
defaultBranch = opts.SingleBranch | |||
} | |||
repo := &repo_model.Repository{ | |||
OwnerID: owner.ID, | |||
Owner: owner, | |||
OwnerName: owner.Name, | |||
Name: opts.Name, | |||
LowerName: strings.ToLower(opts.Name), | |||
Description: opts.Description, | |||
DefaultBranch: defaultBranch, | |||
IsPrivate: opts.BaseRepo.IsPrivate || opts.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate, | |||
IsEmpty: opts.BaseRepo.IsEmpty, | |||
IsFork: true, | |||
ForkID: opts.BaseRepo.ID, | |||
OwnerID: owner.ID, | |||
Owner: owner, | |||
OwnerName: owner.Name, | |||
Name: opts.Name, | |||
LowerName: strings.ToLower(opts.Name), | |||
Description: opts.Description, | |||
DefaultBranch: defaultBranch, | |||
IsPrivate: opts.BaseRepo.IsPrivate || opts.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate, | |||
IsEmpty: opts.BaseRepo.IsEmpty, | |||
IsFork: true, | |||
ForkID: opts.BaseRepo.ID, | |||
ObjectFormatName: opts.BaseRepo.ObjectFormatName, | |||
} | |||
oldRepoPath := opts.BaseRepo.RepoPath() |
@@ -67,6 +67,9 @@ | |||
{{if .IsTemplate}} | |||
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.template"}}</span> | |||
{{end}} | |||
{{if eq .ObjectFormatName "sha256"}} | |||
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.sha256"}}</span> | |||
{{end}} | |||
{{if .IsMirror}} | |||
{{svg "octicon-mirror"}} | |||
{{else if .IsFork}} |
@@ -25,6 +25,9 @@ | |||
{{if .IsTemplate}} | |||
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.template"}}</span> | |||
{{end}} | |||
{{if eq .ObjectFormatName "sha256"}} | |||
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.sha256"}}</span> | |||
{{end}} | |||
</span> | |||
</div> | |||
<div class="flex-item-trailing"> |
@@ -3,7 +3,7 @@ | |||
<thead> | |||
<tr> | |||
<th class="three wide">{{ctx.Locale.Tr "repo.commits.author"}}</th> | |||
<th class="two wide sha">SHA1</th> | |||
<th class="two wide sha">{{StringUtils.ToUpper $.Repository.ObjectFormatName}}</th> | |||
<th class="eight wide message">{{ctx.Locale.Tr "repo.commits.message"}}</th> | |||
<th class="two wide right aligned">{{ctx.Locale.Tr "repo.commits.date"}}</th> | |||
<th class="one wide"></th> |
@@ -185,6 +185,19 @@ | |||
<input id="default_branch" name="default_branch" value="{{.default_branch}}" placeholder="{{.default_branch}}"> | |||
<span class="help">{{ctx.Locale.Tr "repo.default_branch_helper"}}</span> | |||
</div> | |||
<div class="inline field"> | |||
<label>{{ctx.Locale.Tr "repo.object_format"}}</label> | |||
<div class="ui selection owner dropdown"> | |||
<input type="hidden" id="object_format_name" name="object_format_name" value="{{.DefaultObjectFormat.Name}}" required> | |||
<div class="default text">{{.DefaultObjectFormat.Name}}</div> | |||
<div class="menu"> | |||
{{range .SupportedObjectFormats}} | |||
<div class="item" data-value="{{.Name}}">{{.Name}}</div> | |||
{{end}} | |||
</div> | |||
</div> | |||
<span class="help">{{ctx.Locale.Tr "repo.object_format_helper"}}</span> | |||
</div> | |||
<div class="inline field"> | |||
<label>{{ctx.Locale.Tr "repo.template"}}</label> | |||
<div class="ui checkbox"> |
@@ -27,6 +27,9 @@ | |||
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.template"}}</span> | |||
<div class="repo-icon" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.template"}}">{{svg "octicon-repo-template" 18}}</div> | |||
{{end}} | |||
{{if eq .ObjectFormatName "sha256"}} | |||
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.sha256"}}</span> | |||
{{end}} | |||
</div> | |||
</div> | |||
{{if not (or .IsBeingCreated .IsBroken)}} |
@@ -18370,6 +18370,15 @@ | |||
"uniqueItems": true, | |||
"x-go-name": "Name" | |||
}, | |||
"object_format_name": { | |||
"description": "ObjectFormatName of the underlying git repository", | |||
"type": "string", | |||
"enum": [ | |||
"sha1", | |||
"sha256" | |||
], | |||
"x-go-name": "ObjectFormatName" | |||
}, | |||
"private": { | |||
"description": "Whether the repository is private", | |||
"type": "boolean", | |||
@@ -22185,6 +22194,15 @@ | |||
"type": "string", | |||
"x-go-name": "Name" | |||
}, | |||
"object_format_name": { | |||
"description": "ObjectFormatName of the underlying git repository", | |||
"type": "string", | |||
"enum": [ | |||
"sha1", | |||
"sha256" | |||
], | |||
"x-go-name": "ObjectFormatName" | |||
}, | |||
"open_issues_count": { | |||
"type": "integer", | |||
"format": "int64", |