Skip to content

Commit 8e71edf

Browse files
committed
git: Add strict checks for supported extensions
The upstream Git enforces fail-safe heuristics to ensure that older git versions will avoid handling repositories using extensions they are unaware of. The logic is largely based on the value of core.repositoryformatversion. As per official Git docs: > This version specifies the rules for operating on the on-disk repository data. > An implementation of git which does not understand a particular version > advertised by an on-disk repository MUST NOT operate on that repository; > doing so risks not only producing wrong results, but actually losing data. Now go-git will ensure that: - The git.Open logic will verify and enforces the extension support rules. - go-git will keep track of built-in extensions that it supports. This is a breaking change and it will force go-git to not be able to open repositories that it in fact doesn't really support. Conversaly, the error messages will be more useful (e.g. unknown extension vs object not found). Upstream refs: - https://git-scm.com/docs/git-config#Documentation/git-config.txt-extensions - https://git-scm.com/docs/gitrepository-layout#_git_repository_format_versions Signed-off-by: Paulo Gomes <[email protected]>
1 parent 438a37f commit 8e71edf

3 files changed

Lines changed: 224 additions & 0 deletions

File tree

repository.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,12 @@ func Open(s storage.Storer, worktree billy.Filesystem) (*Repository, error) {
208208
return nil, ErrRepositoryNotExists
209209
}
210210

211+
cfg, err := s.Config()
212+
if err != nil {
213+
return nil, err
214+
}
215+
216+
err = verifyExtensions(s, cfg)
211217
if err != nil {
212218
return nil, err
213219
}

repository_extensions.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package git
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/go-git/go-git/v5/config"
9+
cfgformat "github.com/go-git/go-git/v5/plumbing/format/config"
10+
"github.com/go-git/go-git/v5/storage"
11+
)
12+
13+
var (
14+
// ErrUnsupportedExtensionRepositoryFormatVersion represents when an
15+
// extension being used is not compatible with the repository's
16+
// core.repositoryFormatVersion.
17+
ErrUnsupportedExtensionRepositoryFormatVersion = errors.New("core.repositoryformatversion does not support extension")
18+
19+
// ErrUnsupportedRepositoryFormatVersion represents when an repository
20+
// is using a format version that is not supported.
21+
ErrUnsupportedRepositoryFormatVersion = errors.New("core.repositoryformatversion not supported")
22+
23+
// ErrUnknownExtension represents when a repository has an extension
24+
// which is unknown or unsupported by go-git.
25+
ErrUnknownExtension = errors.New("unknown extension")
26+
27+
// builtinExtensions defines the Git extensions that are supported by
28+
// the core go-git implementation.
29+
//
30+
// Some extensions are storage-specific, those are defined by the Storers
31+
// themselves by implementing the ExtensionChecker interface.
32+
builtinExtensions = map[string]struct{}{
33+
// noop does not change git’s behavior at all.
34+
// It is useful only for testing format-1 compatibility.
35+
//
36+
// This extension is respected regardless of the
37+
// core.repositoryFormatVersion setting.
38+
"noop": {},
39+
40+
// noop-v1 does not change git’s behavior at all.
41+
// It is useful only for testing format-1 compatibility.
42+
"noop-v1": {},
43+
}
44+
45+
// Some Git extensions were supported upstream before the introduction
46+
// of repositoryformatversion. These are the only extensions that can be
47+
// enabled while core.repositoryformatversion is unset or set to 0.
48+
extensionsValidForV0 = map[string]struct{}{
49+
"noop": {},
50+
"partialClone": {},
51+
"preciousObjects": {},
52+
"worktreeConfig": {},
53+
}
54+
)
55+
56+
type extension struct {
57+
name string
58+
value string
59+
}
60+
61+
func extensions(cfg *config.Config) []extension {
62+
if cfg == nil || cfg.Raw == nil {
63+
return nil
64+
}
65+
66+
if !cfg.Raw.HasSection("extensions") {
67+
return nil
68+
}
69+
70+
section := cfg.Raw.Section("extensions")
71+
out := make([]extension, 0, len(section.Options))
72+
for _, opt := range section.Options {
73+
out = append(out, extension{name: strings.ToLower(opt.Key), value: strings.ToLower(opt.Value)})
74+
}
75+
76+
return out
77+
}
78+
79+
func verifyExtensions(st storage.Storer, cfg *config.Config) error {
80+
needed := extensions(cfg)
81+
82+
switch cfg.Core.RepositoryFormatVersion {
83+
case "", cfgformat.Version_0, cfgformat.Version_1:
84+
default:
85+
return fmt.Errorf("%w: %q",
86+
ErrUnsupportedRepositoryFormatVersion,
87+
cfg.Core.RepositoryFormatVersion)
88+
}
89+
90+
if len(needed) > 0 {
91+
if cfg.Core.RepositoryFormatVersion == cfgformat.Version_0 ||
92+
cfg.Core.RepositoryFormatVersion == "" {
93+
var unsupported []string
94+
for _, ext := range needed {
95+
if _, ok := extensionsValidForV0[ext.name]; !ok {
96+
unsupported = append(unsupported, ext.name)
97+
}
98+
}
99+
if len(unsupported) > 0 {
100+
return fmt.Errorf("%w: %s",
101+
ErrUnsupportedExtensionRepositoryFormatVersion,
102+
strings.Join(unsupported, ", "))
103+
}
104+
}
105+
106+
var missing []string
107+
for _, ext := range needed {
108+
if _, ok := builtinExtensions[ext.name]; ok {
109+
continue
110+
}
111+
112+
missing = append(missing, ext.name)
113+
}
114+
115+
if len(missing) > 0 {
116+
return fmt.Errorf("%w: %s", ErrUnknownExtension, strings.Join(missing, ", "))
117+
}
118+
}
119+
120+
return nil
121+
}

repository_extensions_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package git
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/go-git/go-git/v5/config"
10+
formatcfg "github.com/go-git/go-git/v5/plumbing/format/config"
11+
"github.com/go-git/go-git/v5/storage/memory"
12+
)
13+
14+
func TestVerifyExtensions(t *testing.T) {
15+
t.Parallel()
16+
17+
tests := []struct {
18+
name string
19+
setup func(*testing.T, *config.Config)
20+
wantErr string
21+
}{
22+
{
23+
name: "repositoryformatversion=0: invalid extension",
24+
setup: func(t *testing.T, cfg *config.Config) {
25+
cfg.Core.RepositoryFormatVersion = formatcfg.Version_0
26+
cfg.Raw.Section("extensions").SetOption("unknown", "foo")
27+
cfg.Raw.Section("extensions").SetOption("objectformat", "sha1")
28+
},
29+
wantErr: "repositoryformatversion does not support extension: unknown, objectformat",
30+
},
31+
{
32+
name: "repositoryformatversion=0: allows supported noop",
33+
setup: func(t *testing.T, cfg *config.Config) {
34+
cfg.Core.RepositoryFormatVersion = formatcfg.Version_0
35+
cfg.Raw.Section("extensions").SetOption("noop", "bar")
36+
},
37+
},
38+
{
39+
name: "repositoryformatversion='': allows supported noop",
40+
setup: func(t *testing.T, cfg *config.Config) {
41+
cfg.Raw.Section("extensions").SetOption("noop", "bar")
42+
},
43+
},
44+
{
45+
name: "repositoryformatversion=1: rejects unknown extensions",
46+
setup: func(t *testing.T, cfg *config.Config) {
47+
cfg.Core.RepositoryFormatVersion = formatcfg.Version_1
48+
cfg.Raw.Section("extensions").SetOption("unknownext", "true")
49+
},
50+
wantErr: "unknown extension: unknownext",
51+
},
52+
{
53+
name: "repositoryformatversion=1: allows known extension",
54+
setup: func(t *testing.T, cfg *config.Config) {
55+
cfg.Core.RepositoryFormatVersion = formatcfg.Version_1
56+
cfg.Raw.Section("extensions").SetOption("NOOP", "foo")
57+
cfg.Raw.Section("extensions").SetOption("noop-v1", "bar")
58+
},
59+
},
60+
{
61+
name: "repositoryformatversion=1: rejects objectformat=sha1", // not supported in go-git/v5
62+
setup: func(t *testing.T, cfg *config.Config) {
63+
cfg.Core.RepositoryFormatVersion = formatcfg.Version_1
64+
cfg.Raw.Section("extensions").SetOption("objectformat", "sha1")
65+
},
66+
wantErr: "unknown extension: objectformat",
67+
},
68+
}
69+
70+
for _, tt := range tests {
71+
t.Run(tt.name, func(t *testing.T) {
72+
t.Parallel()
73+
74+
st := memory.NewStorage()
75+
76+
r, err := Init(st, nil)
77+
require.NoError(t, err)
78+
require.NotNil(t, r)
79+
80+
cfg, err := st.Config()
81+
require.NoError(t, err)
82+
83+
tt.setup(t, cfg)
84+
require.NoError(t, st.SetConfig(cfg))
85+
86+
r, err = Open(st, nil)
87+
if tt.wantErr != "" {
88+
require.Error(t, err)
89+
assert.Contains(t, err.Error(), tt.wantErr)
90+
assert.Nil(t, r)
91+
} else {
92+
require.NoError(t, err)
93+
assert.NotNil(t, r)
94+
}
95+
})
96+
}
97+
}

0 commit comments

Comments
 (0)