mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-07-25 12:39:40 +02:00
git/commit: re-implement submodules file reader (#8438)
Reimplement the submodules parser to not depend on the go-git dependency. See #8222 for the full refactor context. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8438 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: oliverpool <git@olivier.pfad.fr> Co-committed-by: oliverpool <git@olivier.pfad.fr>
This commit is contained in:
parent
48cc6e684a
commit
5158493ba6
17 changed files with 220 additions and 166 deletions
|
@ -16,8 +16,6 @@ import (
|
|||
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/util"
|
||||
|
||||
"github.com/go-git/go-git/v5/config"
|
||||
)
|
||||
|
||||
// Commit represents a git commit.
|
||||
|
@ -29,8 +27,8 @@ type Commit struct {
|
|||
CommitMessage string
|
||||
Signature *ObjectSignature
|
||||
|
||||
Parents []ObjectID // ID strings
|
||||
submoduleCache *ObjectCache
|
||||
Parents []ObjectID // ID strings
|
||||
submodules map[string]Submodule // submodule indexed by path
|
||||
}
|
||||
|
||||
// Message returns the commit message. Same as retrieving CommitMessage directly.
|
||||
|
@ -352,64 +350,6 @@ func (c *Commit) GetFileContent(filename string, limit int) (string, error) {
|
|||
return string(bytes), nil
|
||||
}
|
||||
|
||||
// GetSubModules get all the sub modules of current revision git tree
|
||||
func (c *Commit) GetSubModules() (*ObjectCache, error) {
|
||||
if c.submoduleCache != nil {
|
||||
return c.submoduleCache, nil
|
||||
}
|
||||
|
||||
entry, err := c.GetTreeEntryByPath(".gitmodules")
|
||||
if err != nil {
|
||||
if _, ok := err.(ErrNotExist); ok {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content, err := entry.Blob().GetBlobContent(10 * 1024)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.submoduleCache, err = parseSubmoduleContent([]byte(content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.submoduleCache, nil
|
||||
}
|
||||
|
||||
func parseSubmoduleContent(bs []byte) (*ObjectCache, error) {
|
||||
cfg := config.NewModules()
|
||||
if err := cfg.Unmarshal(bs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
submoduleCache := newObjectCache()
|
||||
if len(cfg.Submodules) == 0 {
|
||||
return nil, errors.New("no submodules found")
|
||||
}
|
||||
for _, subModule := range cfg.Submodules {
|
||||
submoduleCache.Set(subModule.Path, subModule.URL)
|
||||
}
|
||||
|
||||
return submoduleCache, nil
|
||||
}
|
||||
|
||||
// GetSubModule returns the URL to the submodule according entryname
|
||||
func (c *Commit) GetSubModule(entryname string) (string, error) {
|
||||
modules, err := c.GetSubModules()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if modules != nil {
|
||||
module, has := modules.Get(entryname)
|
||||
if has {
|
||||
return module.(string), nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only')
|
||||
func (c *Commit) GetBranchName() (string, error) {
|
||||
cmd := NewCommand(c.repo.Ctx, "name-rev", "--exclude", "refs/tags/*", "--name-only", "--no-undefined").AddDynamicArguments(c.ID.String())
|
||||
|
|
|
@ -15,9 +15,9 @@ import (
|
|||
|
||||
// CommitInfo describes the first commit with the provided entry
|
||||
type CommitInfo struct {
|
||||
Entry *TreeEntry
|
||||
Commit *Commit
|
||||
SubModuleFile *SubModuleFile
|
||||
Entry *TreeEntry
|
||||
Commit *Commit
|
||||
Submodule Submodule
|
||||
}
|
||||
|
||||
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
||||
|
@ -71,19 +71,18 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
|
|||
}
|
||||
|
||||
// If the entry if a submodule add a submodule file for this
|
||||
if entry.IsSubModule() {
|
||||
if entry.IsSubmodule() {
|
||||
var fullPath string
|
||||
if len(treePath) > 0 {
|
||||
fullPath = treePath + "/" + entry.Name()
|
||||
} else {
|
||||
fullPath = entry.Name()
|
||||
}
|
||||
subModuleURL, err := commit.GetSubModule(fullPath)
|
||||
submodule, err := commit.GetSubmodule(fullPath, entry)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String())
|
||||
commitsInfo[i].SubModuleFile = subModuleFile
|
||||
commitsInfo[i].Submodule = submodule
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -436,33 +436,3 @@ func TestGetAllBranches(t *testing.T) {
|
|||
|
||||
assert.Equal(t, []string{"branch1", "branch2", "master"}, branches)
|
||||
}
|
||||
|
||||
func Test_parseSubmoduleContent(t *testing.T) {
|
||||
submoduleFiles := []struct {
|
||||
fileContent string
|
||||
expectedPath string
|
||||
expectedURL string
|
||||
}{
|
||||
{
|
||||
fileContent: `[submodule "jakarta-servlet"]
|
||||
url = ../../ALP-pool/jakarta-servlet
|
||||
path = jakarta-servlet`,
|
||||
expectedPath: "jakarta-servlet",
|
||||
expectedURL: "../../ALP-pool/jakarta-servlet",
|
||||
},
|
||||
{
|
||||
fileContent: `[submodule "jakarta-servlet"]
|
||||
path = jakarta-servlet
|
||||
url = ../../ALP-pool/jakarta-servlet`,
|
||||
expectedPath: "jakarta-servlet",
|
||||
expectedURL: "../../ALP-pool/jakarta-servlet",
|
||||
},
|
||||
}
|
||||
for _, kase := range submoduleFiles {
|
||||
submodule, err := parseSubmoduleContent([]byte(kase.fileContent))
|
||||
require.NoError(t, err)
|
||||
v, ok := submodule.Get(kase.expectedPath)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, kase.expectedURL, v)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,38 +6,124 @@ package git
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/util"
|
||||
|
||||
"gopkg.in/ini.v1" //nolint:depguard // used to read .gitmodules
|
||||
)
|
||||
|
||||
var scpSyntax = regexp.MustCompile(`^([a-zA-Z0-9_]+@)?([a-zA-Z0-9._-]+):(.*)$`)
|
||||
|
||||
// SubModule submodule is a reference on git repository
|
||||
type SubModule struct {
|
||||
Name string
|
||||
URL string
|
||||
}
|
||||
|
||||
// SubModuleFile represents a file with submodule type.
|
||||
type SubModuleFile struct {
|
||||
*Commit
|
||||
|
||||
refURL string
|
||||
refID string
|
||||
}
|
||||
|
||||
// NewSubModuleFile create a new submodule file
|
||||
func NewSubModuleFile(c *Commit, refURL, refID string) *SubModuleFile {
|
||||
return &SubModuleFile{
|
||||
Commit: c,
|
||||
refURL: refURL,
|
||||
refID: refID,
|
||||
// GetSubmodule returns the Submodule of a given path
|
||||
func (c *Commit) GetSubmodule(path string, entry *TreeEntry) (Submodule, error) {
|
||||
err := c.readSubmodules()
|
||||
if err != nil {
|
||||
// the .gitmodules file exists but could not be read or parsed
|
||||
return Submodule{}, err
|
||||
}
|
||||
|
||||
sm, ok := c.submodules[path]
|
||||
if !ok {
|
||||
// no info found in .gitmodules: fallback to what we can provide
|
||||
return Submodule{
|
||||
Path: path,
|
||||
Commit: entry.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
sm.Commit = entry.ID
|
||||
return sm, nil
|
||||
}
|
||||
|
||||
// readSubmodules populates the submodules field by reading the .gitmodules file
|
||||
func (c *Commit) readSubmodules() error {
|
||||
if c.submodules != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
entry, err := c.GetTreeEntryByPath(".gitmodules")
|
||||
if err != nil {
|
||||
if IsErrNotExist(err) {
|
||||
c.submodules = make(map[string]Submodule)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
rc, _, err := entry.Blob().NewTruncatedReader(10 * 1024)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
c.submodules, err = parseSubmoduleContent(rc)
|
||||
return err
|
||||
}
|
||||
|
||||
func parseSubmoduleContent(r io.Reader) (map[string]Submodule, error) {
|
||||
// https://git-scm.com/docs/gitmodules#_description
|
||||
// The .gitmodules file, located in the top-level directory of a Git working tree
|
||||
// is a text file with a syntax matching the requirements of git-config[1].
|
||||
// https://git-scm.com/docs/git-config#_configuration_file
|
||||
|
||||
cfg := ini.Empty(ini.LoadOptions{
|
||||
InsensitiveKeys: true, // "The variable names are case-insensitive", but "Subsection names are case sensitive"
|
||||
})
|
||||
err := cfg.Append(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sections := cfg.Sections()
|
||||
submodule := make(map[string]Submodule, len(sections))
|
||||
|
||||
for _, s := range sections {
|
||||
sm := parseSubmoduleSection(s)
|
||||
if sm.Path == "" || sm.URL == "" {
|
||||
continue
|
||||
}
|
||||
submodule[sm.Path] = sm
|
||||
}
|
||||
return submodule, nil
|
||||
}
|
||||
|
||||
func parseSubmoduleSection(s *ini.Section) Submodule {
|
||||
section, name, _ := strings.Cut(s.Name(), " ")
|
||||
if !util.ASCIIEqualFold("submodule", section) { // See https://codeberg.org/forgejo/forgejo/pulls/8438#issuecomment-5805251
|
||||
return Submodule{}
|
||||
}
|
||||
_ = name
|
||||
|
||||
sm := Submodule{}
|
||||
if key, _ := s.GetKey("path"); key != nil {
|
||||
sm.Path = key.Value()
|
||||
}
|
||||
if key, _ := s.GetKey("url"); key != nil {
|
||||
sm.URL = key.Value()
|
||||
}
|
||||
return sm
|
||||
}
|
||||
|
||||
// Submodule represents a parsed git submodule reference.
|
||||
type Submodule struct {
|
||||
Path string // path property
|
||||
URL string // upstream URL
|
||||
Commit ObjectID // upstream Commit-ID
|
||||
}
|
||||
|
||||
// ResolveUpstreamURL resolves the upstream URL relative to the repo URL.
|
||||
func (sm Submodule) ResolveUpstreamURL(repoURL string) string {
|
||||
repoFullName := strings.TrimPrefix(repoURL, setting.AppURL) // currently hacky, but can be dropped when refactoring getRefURL
|
||||
return getRefURL(sm.URL, setting.AppURL, repoFullName, setting.SSH.Domain)
|
||||
}
|
||||
|
||||
var scpSyntax = regexp.MustCompile(`^([a-zA-Z0-9_]+@)?([a-zA-Z0-9._-]+):(.*)$`)
|
||||
|
||||
func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string {
|
||||
if refURL == "" {
|
||||
return ""
|
||||
|
@ -53,7 +139,7 @@ func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string {
|
|||
|
||||
urlPrefix = strings.TrimSuffix(urlPrefix, "/")
|
||||
|
||||
// FIXME: Need to consider branch - which will require changes in modules/git/commit.go:GetSubModules
|
||||
// FIXME: Need to consider branch - which will require changes in parseSubmoduleSection
|
||||
// Relative url prefix check (according to git submodule documentation)
|
||||
if strings.HasPrefix(refURI, "./") || strings.HasPrefix(refURI, "../") {
|
||||
return urlPrefix + path.Clean(path.Join("/", repoFullName, refURI))
|
||||
|
@ -107,13 +193,3 @@ func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string {
|
|||
|
||||
return ""
|
||||
}
|
||||
|
||||
// RefURL guesses and returns reference URL.
|
||||
func (sf *SubModuleFile) RefURL(urlPrefix, repoFullName, sshDomain string) string {
|
||||
return getRefURL(sf.refURL, urlPrefix, repoFullName, sshDomain)
|
||||
}
|
||||
|
||||
// RefID returns reference ID.
|
||||
func (sf *SubModuleFile) RefID() string {
|
||||
return sf.refID
|
||||
}
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetRefURL(t *testing.T) {
|
||||
|
@ -40,3 +42,74 @@ func TestGetRefURL(t *testing.T) {
|
|||
assert.Equal(t, kase.expect, getRefURL(kase.refURL, kase.prefixURL, kase.parentPath, kase.SSHDomain))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseSubmoduleContent(t *testing.T) {
|
||||
submoduleFiles := []struct {
|
||||
fileContent string
|
||||
expectedPath string
|
||||
expected Submodule
|
||||
}{
|
||||
{
|
||||
fileContent: `[submodule "jakarta-servlet"]
|
||||
url = ../../ALP-pool/jakarta-servlet
|
||||
path = jakarta-servlet`,
|
||||
expectedPath: "jakarta-servlet",
|
||||
expected: Submodule{
|
||||
Path: "jakarta-servlet",
|
||||
URL: "../../ALP-pool/jakarta-servlet",
|
||||
},
|
||||
},
|
||||
{
|
||||
fileContent: `[submodule "jakarta-servlet"]
|
||||
path = jakarta-servlet
|
||||
url = ../../ALP-pool/jakarta-servlet`,
|
||||
expectedPath: "jakarta-servlet",
|
||||
expected: Submodule{
|
||||
Path: "jakarta-servlet",
|
||||
URL: "../../ALP-pool/jakarta-servlet",
|
||||
},
|
||||
},
|
||||
{
|
||||
fileContent: `[submodule "about/documents"]
|
||||
path = about/documents
|
||||
url = git@github.com:example/documents.git
|
||||
branch = gh-pages
|
||||
[submodule "custom-name"]
|
||||
path = manifesto
|
||||
url = https://github.com/example/manifesto.git
|
||||
[submodule]
|
||||
path = relative/url
|
||||
url = ../such-relative.git
|
||||
`,
|
||||
expectedPath: "relative/url",
|
||||
expected: Submodule{
|
||||
Path: "relative/url",
|
||||
URL: "../such-relative.git",
|
||||
},
|
||||
},
|
||||
{
|
||||
fileContent: `# .gitmodules
|
||||
# Subsection names are case sensitive
|
||||
[submodule "Seanpm2001/Degoogle-your-life"]
|
||||
path = Its-time-to-cut-WideVine-DRM/DeGoogle-Your-Life/submodule.gitmodules
|
||||
url = https://github.com/seanpm2001/Degoogle-your-life/
|
||||
|
||||
[submodule "seanpm2001/degoogle-your-life"]
|
||||
url = https://github.com/seanpm2001/degoogle-your-life/
|
||||
# This second section should not be merged with the first, because of casing
|
||||
`,
|
||||
expectedPath: "Its-time-to-cut-WideVine-DRM/DeGoogle-Your-Life/submodule.gitmodules",
|
||||
expected: Submodule{
|
||||
Path: "Its-time-to-cut-WideVine-DRM/DeGoogle-Your-Life/submodule.gitmodules",
|
||||
URL: "https://github.com/seanpm2001/Degoogle-your-life/",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, kase := range submoduleFiles {
|
||||
submodule, err := parseSubmoduleContent(strings.NewReader(kase.fileContent))
|
||||
require.NoError(t, err)
|
||||
v, ok := submodule[kase.expectedPath]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, kase.expected, v)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
|||
ptree: t,
|
||||
ID: t.ID,
|
||||
name: "",
|
||||
fullName: "",
|
||||
entryMode: EntryModeTree,
|
||||
}, nil
|
||||
}
|
||||
|
@ -55,7 +54,7 @@ func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if !entry.IsDir() && !entry.IsSubModule() {
|
||||
if !entry.IsDir() && !entry.IsSubmodule() {
|
||||
return entry.Blob(), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -21,16 +21,12 @@ type TreeEntry struct {
|
|||
entryMode EntryMode
|
||||
name string
|
||||
|
||||
size int64
|
||||
sized bool
|
||||
fullName string
|
||||
size int64
|
||||
sized bool
|
||||
}
|
||||
|
||||
// Name returns the name of the entry
|
||||
func (te *TreeEntry) Name() string {
|
||||
if te.fullName != "" {
|
||||
return te.fullName
|
||||
}
|
||||
return te.name
|
||||
}
|
||||
|
||||
|
@ -68,8 +64,8 @@ func (te *TreeEntry) Size() int64 {
|
|||
return te.size
|
||||
}
|
||||
|
||||
// IsSubModule if the entry is a sub module
|
||||
func (te *TreeEntry) IsSubModule() bool {
|
||||
// IsSubmodule if the entry is a submodule
|
||||
func (te *TreeEntry) IsSubmodule() bool {
|
||||
return te.entryMode == EntryModeCommit
|
||||
}
|
||||
|
||||
|
@ -214,7 +210,7 @@ func (te *TreeEntry) Tree() *Tree {
|
|||
|
||||
// GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory )
|
||||
func (te *TreeEntry) GetSubJumpablePathName() string {
|
||||
if te.IsSubModule() || !te.IsDir() {
|
||||
if te.IsSubmodule() || !te.IsDir() {
|
||||
return ""
|
||||
}
|
||||
tree, err := te.ptree.SubTree(te.Name())
|
||||
|
@ -241,7 +237,7 @@ type customSortableEntries struct {
|
|||
|
||||
var sorter = []func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool{
|
||||
func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
|
||||
return (t1.IsDir() || t1.IsSubModule()) && !t2.IsDir() && !t2.IsSubModule()
|
||||
return (t1.IsDir() || t1.IsSubmodule()) && !t2.IsDir() && !t2.IsSubmodule()
|
||||
},
|
||||
func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
|
||||
return cmp(t1.Name(), t2.Name())
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue