1
0
Fork 0
mirror of https://codeberg.org/forgejo/forgejo.git synced 2025-07-18 17:19:41 +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:
oliverpool 2025-07-15 00:20:00 +02:00 committed by Gusted
parent 48cc6e684a
commit 5158493ba6
17 changed files with 220 additions and 166 deletions

View file

@ -42,6 +42,8 @@ linters:
desc: do not use the ini package, use gitea's config system instead desc: do not use the ini package, use gitea's config system instead
- pkg: github.com/minio/sha256-simd - pkg: github.com/minio/sha256-simd
desc: use crypto/sha256 instead, see https://codeberg.org/forgejo/forgejo/pulls/1528 desc: use crypto/sha256 instead, see https://codeberg.org/forgejo/forgejo/pulls/1528
- pkg: github.com/go-git/go-git
desc: use forgejo.org/modules/git instead, see https://codeberg.org/forgejo/forgejo/pulls/4941
gocritic: gocritic:
disabled-checks: disabled-checks:
- ifElseChain - ifElseChain

2
go.mod
View file

@ -45,7 +45,6 @@ require (
github.com/go-chi/cors v1.2.2 github.com/go-chi/cors v1.2.2
github.com/go-co-op/gocron v1.37.0 github.com/go-co-op/gocron v1.37.0
github.com/go-enry/go-enry/v2 v2.9.2 github.com/go-enry/go-enry/v2 v2.9.2
github.com/go-git/go-git/v5 v5.13.2
github.com/go-ldap/ldap/v3 v3.4.6 github.com/go-ldap/ldap/v3 v3.4.6
github.com/go-openapi/spec v0.21.0 github.com/go-openapi/spec v0.21.0
github.com/go-sql-driver/mysql v1.9.3 github.com/go-sql-driver/mysql v1.9.3
@ -166,6 +165,7 @@ require (
github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.13.2 // indirect
github.com/go-ini/ini v1.67.0 // indirect github.com/go-ini/ini v1.67.0 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect

View file

@ -114,7 +114,7 @@ func EntryIcon(entry *git.TreeEntry) string {
return "file-symlink-file" return "file-symlink-file"
case entry.IsDir(): case entry.IsDir():
return "file-directory-fill" return "file-directory-fill"
case entry.IsSubModule(): case entry.IsSubmodule():
return "file-submodule" return "file-submodule"
} }

View file

@ -16,8 +16,6 @@ import (
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/modules/util" "forgejo.org/modules/util"
"github.com/go-git/go-git/v5/config"
) )
// Commit represents a git commit. // Commit represents a git commit.
@ -29,8 +27,8 @@ type Commit struct {
CommitMessage string CommitMessage string
Signature *ObjectSignature Signature *ObjectSignature
Parents []ObjectID // ID strings Parents []ObjectID // ID strings
submoduleCache *ObjectCache submodules map[string]Submodule // submodule indexed by path
} }
// Message returns the commit message. Same as retrieving CommitMessage directly. // 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 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') // GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only')
func (c *Commit) GetBranchName() (string, error) { func (c *Commit) GetBranchName() (string, error) {
cmd := NewCommand(c.repo.Ctx, "name-rev", "--exclude", "refs/tags/*", "--name-only", "--no-undefined").AddDynamicArguments(c.ID.String()) cmd := NewCommand(c.repo.Ctx, "name-rev", "--exclude", "refs/tags/*", "--name-only", "--no-undefined").AddDynamicArguments(c.ID.String())

View file

@ -15,9 +15,9 @@ import (
// CommitInfo describes the first commit with the provided entry // CommitInfo describes the first commit with the provided entry
type CommitInfo struct { type CommitInfo struct {
Entry *TreeEntry Entry *TreeEntry
Commit *Commit Commit *Commit
SubModuleFile *SubModuleFile Submodule Submodule
} }
// GetCommitsInfo gets information of all commits that are corresponding to these entries // 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 the entry if a submodule add a submodule file for this
if entry.IsSubModule() { if entry.IsSubmodule() {
var fullPath string var fullPath string
if len(treePath) > 0 { if len(treePath) > 0 {
fullPath = treePath + "/" + entry.Name() fullPath = treePath + "/" + entry.Name()
} else { } else {
fullPath = entry.Name() fullPath = entry.Name()
} }
subModuleURL, err := commit.GetSubModule(fullPath) submodule, err := commit.GetSubmodule(fullPath, entry)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String()) commitsInfo[i].Submodule = submodule
commitsInfo[i].SubModuleFile = subModuleFile
} }
} }

View file

@ -436,33 +436,3 @@ func TestGetAllBranches(t *testing.T) {
assert.Equal(t, []string{"branch1", "branch2", "master"}, branches) 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)
}
}

View file

@ -6,38 +6,124 @@ package git
import ( import (
"fmt" "fmt"
"io"
"net" "net"
"net/url" "net/url"
"path" "path"
"regexp" "regexp"
"strings" "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._-]+):(.*)$`) // GetSubmodule returns the Submodule of a given path
func (c *Commit) GetSubmodule(path string, entry *TreeEntry) (Submodule, error) {
// SubModule submodule is a reference on git repository err := c.readSubmodules()
type SubModule struct { if err != nil {
Name string // the .gitmodules file exists but could not be read or parsed
URL string return Submodule{}, err
}
// 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,
} }
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 { func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string {
if refURL == "" { if refURL == "" {
return "" return ""
@ -53,7 +139,7 @@ func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string {
urlPrefix = strings.TrimSuffix(urlPrefix, "/") 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) // Relative url prefix check (according to git submodule documentation)
if strings.HasPrefix(refURI, "./") || strings.HasPrefix(refURI, "../") { if strings.HasPrefix(refURI, "./") || strings.HasPrefix(refURI, "../") {
return urlPrefix + path.Clean(path.Join("/", repoFullName, refURI)) return urlPrefix + path.Clean(path.Join("/", repoFullName, refURI))
@ -107,13 +193,3 @@ func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string {
return "" 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
}

View file

@ -4,9 +4,11 @@
package git package git
import ( import (
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestGetRefURL(t *testing.T) { 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)) 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)
}
}

View file

@ -17,7 +17,6 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
ptree: t, ptree: t,
ID: t.ID, ID: t.ID,
name: "", name: "",
fullName: "",
entryMode: EntryModeTree, entryMode: EntryModeTree,
}, nil }, nil
} }
@ -55,7 +54,7 @@ func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) {
return nil, err return nil, err
} }
if !entry.IsDir() && !entry.IsSubModule() { if !entry.IsDir() && !entry.IsSubmodule() {
return entry.Blob(), nil return entry.Blob(), nil
} }

View file

@ -21,16 +21,12 @@ type TreeEntry struct {
entryMode EntryMode entryMode EntryMode
name string name string
size int64 size int64
sized bool sized bool
fullName string
} }
// Name returns the name of the entry // Name returns the name of the entry
func (te *TreeEntry) Name() string { func (te *TreeEntry) Name() string {
if te.fullName != "" {
return te.fullName
}
return te.name return te.name
} }
@ -68,8 +64,8 @@ func (te *TreeEntry) Size() int64 {
return te.size return te.size
} }
// IsSubModule if the entry is a sub module // IsSubmodule if the entry is a submodule
func (te *TreeEntry) IsSubModule() bool { func (te *TreeEntry) IsSubmodule() bool {
return te.entryMode == EntryModeCommit 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 ) // GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory )
func (te *TreeEntry) GetSubJumpablePathName() string { func (te *TreeEntry) GetSubJumpablePathName() string {
if te.IsSubModule() || !te.IsDir() { if te.IsSubmodule() || !te.IsDir() {
return "" return ""
} }
tree, err := te.ptree.SubTree(te.Name()) 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{ var sorter = []func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool{
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 { func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
return cmp(t1.Name(), t2.Name()) return cmp(t1.Name(), t2.Name())

View file

@ -241,7 +241,7 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEn
return nil, nil, nil return nil, nil, nil
} }
if entry.IsDir() || entry.IsSubModule() { if entry.IsDir() || entry.IsSubmodule() {
ctx.NotFound("getBlobForEntry", nil) ctx.NotFound("getBlobForEntry", nil)
return nil, nil, nil return nil, nil, nil
} }

View file

@ -92,7 +92,7 @@ func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) {
return nil, nil return nil, nil
} }
if entry.IsDir() || entry.IsSubModule() { if entry.IsDir() || entry.IsSubmodule() {
ctx.NotFound("getBlobForEntry", nil) ctx.NotFound("getBlobForEntry", nil)
return nil, nil return nil, nil
} }

View file

@ -42,7 +42,7 @@ func isExcludedEntry(entry *git.TreeEntry) bool {
return true return true
} }
if entry.IsSubModule() { if entry.IsSubmodule() {
return true return true
} }

View file

@ -1057,14 +1057,13 @@ func renderHomeCode(ctx *context.Context) {
return return
} }
if entry.IsSubModule() { if entry.IsSubmodule() {
subModuleURL, err := ctx.Repo.Commit.GetSubModule(entry.Name()) submodule, err := ctx.Repo.Commit.GetSubmodule(ctx.Repo.TreePath, entry)
if err != nil { if err != nil {
HandleGitError(ctx, "Repo.Commit.GetSubModule", err) HandleGitError(ctx, "Repo.Commit.GetSubmodule", err)
return return
} }
subModuleFile := git.NewSubModuleFile(ctx.Repo.Commit, subModuleURL, entry.ID.String()) ctx.Redirect(submodule.ResolveUpstreamURL(ctx.Repo.Repository.HTMLURL()))
ctx.Redirect(subModuleFile.RefURL(setting.AppURL, ctx.Repo.Repository.FullName(), setting.SSH.Domain))
} else if entry.IsDir() { } else if entry.IsDir() {
renderDirectory(ctx) renderDirectory(ctx)
} else { } else {

View file

@ -108,7 +108,7 @@ func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType {
switch { switch {
case entry.IsDir(): case entry.IsDir():
return ContentTypeDir return ContentTypeDir
case entry.IsSubModule(): case entry.IsSubmodule():
return ContentTypeSubmodule return ContentTypeSubmodule
case entry.IsExecutable(), entry.IsRegular(): case entry.IsExecutable(), entry.IsRegular():
return ContentTypeRegular return ContentTypeRegular
@ -211,14 +211,14 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
return nil, err return nil, err
} }
contentsResponse.Target = &targetFromContent contentsResponse.Target = &targetFromContent
} else if entry.IsSubModule() { } else if entry.IsSubmodule() {
contentsResponse.Type = string(ContentTypeSubmodule) contentsResponse.Type = string(ContentTypeSubmodule)
submoduleURL, err := commit.GetSubModule(treePath) submodule, err := commit.GetSubmodule(treePath, entry)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if submoduleURL != "" { if submodule.URL != "" {
contentsResponse.SubmoduleGitURL = &submoduleURL contentsResponse.SubmoduleGitURL = &submodule.URL
} }
} }
// Handle links // Handle links
@ -230,7 +230,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
downloadURLString := downloadURL.String() downloadURLString := downloadURL.String()
contentsResponse.DownloadURL = &downloadURLString contentsResponse.DownloadURL = &downloadURLString
} }
if !entry.IsSubModule() { if !entry.IsSubmodule() {
htmlURL, err := url.Parse(repo.HTMLURL() + "/src/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath)) htmlURL, err := url.Parse(repo.HTMLURL() + "/src/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath))
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -87,7 +87,7 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
if entries[e].IsDir() { if entries[e].IsDir() {
copy(treeURL[copyPos:], entries[e].ID.String()) copy(treeURL[copyPos:], entries[e].ID.String())
tree.Entries[i].URL = string(treeURL) tree.Entries[i].URL = string(treeURL)
} else if entries[e].IsSubModule() { } else if entries[e].IsSubmodule() {
// In Github Rest API Version=2022-11-28, if a tree entry is a submodule, // In Github Rest API Version=2022-11-28, if a tree entry is a submodule,
// its url will be returned as an empty string. // its url will be returned as an empty string.
// So the URL will be set to "" here. // So the URL will be set to "" here.

View file

@ -20,17 +20,17 @@
{{range $item := .Files}} {{range $item := .Files}}
{{$entry := $item.Entry}} {{$entry := $item.Entry}}
{{$commit := $item.Commit}} {{$commit := $item.Commit}}
{{$subModuleFile := $item.SubModuleFile}}
<tr data-entryname="{{$entry.Name}}" data-ready="{{if $commit}}true{{else}}false{{end}}" class="{{if not $commit}}not{{end}}ready entry"> <tr data-entryname="{{$entry.Name}}" data-ready="{{if $commit}}true{{else}}false{{end}}" class="{{if not $commit}}not{{end}}ready entry">
<td class="name four wide"> <td class="name four wide">
<span class="truncate"> <span class="truncate">
{{if $entry.IsSubModule}} {{if $entry.IsSubmodule}}
{{$refURL := $subModuleFile.RefURL AppUrl $.Repository.FullName $.SSHDomain}} {{/* FIXME: the usage of AppUrl seems incorrect, it would be fixed in the future, use AppSubUrl instead */}} {{$submodule := $item.Submodule}}
{{$refURL := $submodule.ResolveUpstreamURL $.Repository.HTMLURL}}
{{$icon := (svg "octicon-file-submodule" 16 "tw-mr-2")}} {{$icon := (svg "octicon-file-submodule" 16 "tw-mr-2")}}
{{if $refURL}} {{if $refURL}}
<a class="muted" href="{{$refURL}}">{{$icon}}{{$entry.Name}}</a><span class="at">@</span><a href="{{$refURL}}/commit/{{PathEscape $subModuleFile.RefID}}">{{ShortSha $subModuleFile.RefID}}</a> <a class="muted" href="{{$refURL}}">{{$icon}}{{$entry.Name}}</a><span class="at">@</span><a href="{{$refURL}}/commit/{{PathEscape $submodule.Commit.String}}">{{ShortSha $submodule.Commit.String}}</a>
{{else}} {{else}}
{{$icon}}{{$entry.Name}}<span class="at">@</span>{{ShortSha $subModuleFile.RefID}} {{$icon}}{{$entry.Name}}<span class="at">@</span>{{ShortSha $submodule.Commit.String}}
{{end}} {{end}}
{{else}} {{else}}
{{if $entry.IsDir}} {{if $entry.IsDir}}