From 81768675d4e3bca55c2df81121b0b3b1a3a4c5cd Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 2 Jan 2025 12:17:05 +0800
Subject: [PATCH] Inherit submodules from template repository content (#16237)
 (#33068)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Backport #16237 (it more likely a bug fix)

Co-authored-by: Steffen Schröter <steffen@vexar.de>
---
 modules/git/batch_reader.go                   |   6 +-
 modules/git/parse.go                          |  78 +++++++++++
 modules/git/parse_nogogit.go                  |  67 ++-------
 modules/git/pipeline/lfs_nogogit.go           |   2 +-
 modules/git/submodule.go                      |  66 +++++++++
 modules/git/submodule_test.go                 |  48 +++++++
 modules/git/tests/repos/repo4_submodules/HEAD |   1 +
 .../git/tests/repos/repo4_submodules/config   |   4 +
 .../97/c3d30df0e6492348292600920a6482feaebb74 | Bin 0 -> 110 bytes
 .../c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 | Bin 0 -> 112 bytes
 .../e1/e59caba97193d48862d6809912043871f37437 |   2 +
 .../repos/repo4_submodules/refs/heads/master  |   1 +
 modules/git/tree.go                           |   4 +-
 modules/git/tree_blob_nogogit.go              |   1 -
 modules/git/tree_entry_nogogit.go             |  12 +-
 services/repository/generate.go               | 129 +++++++++---------
 templates/repo/view_list.tmpl                 |   4 +-
 17 files changed, 290 insertions(+), 135 deletions(-)
 create mode 100644 modules/git/parse.go
 create mode 100644 modules/git/submodule.go
 create mode 100644 modules/git/submodule_test.go
 create mode 100644 modules/git/tests/repos/repo4_submodules/HEAD
 create mode 100644 modules/git/tests/repos/repo4_submodules/config
 create mode 100644 modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74
 create mode 100644 modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34
 create mode 100644 modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437
 create mode 100644 modules/git/tests/repos/repo4_submodules/refs/heads/master

diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go
index 7dfda72155..4e9f854ad9 100644
--- a/modules/git/batch_reader.go
+++ b/modules/git/batch_reader.go
@@ -253,7 +253,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte {
 	return out
 }
 
-// ParseTreeLine reads an entry from a tree in a cat-file --batch stream
+// ParseCatFileTreeLine reads an entry from a tree in a cat-file --batch stream
 // This carefully avoids allocations - except where fnameBuf is too small.
 // It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations
 //
@@ -261,7 +261,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte {
 // <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <binary HASH>
 //
 // We don't attempt to convert the raw HASH to save a lot of time
-func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
+func ParseCatFileTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
 	var readBytes []byte
 
 	// Read the Mode & fname
@@ -271,7 +271,7 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu
 	}
 	idx := bytes.IndexByte(readBytes, ' ')
 	if idx < 0 {
-		log.Debug("missing space in readBytes ParseTreeLine: %s", readBytes)
+		log.Debug("missing space in readBytes ParseCatFileTreeLine: %s", readBytes)
 		return mode, fname, sha, n, &ErrNotExist{}
 	}
 
diff --git a/modules/git/parse.go b/modules/git/parse.go
new file mode 100644
index 0000000000..eb26632cc0
--- /dev/null
+++ b/modules/git/parse.go
@@ -0,0 +1,78 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"bytes"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"code.gitea.io/gitea/modules/optional"
+)
+
+var sepSpace = []byte{' '}
+
+type LsTreeEntry struct {
+	ID        ObjectID
+	EntryMode EntryMode
+	Name      string
+	Size      optional.Option[int64]
+}
+
+func parseLsTreeLine(line []byte) (*LsTreeEntry, error) {
+	// expect line to be of the form:
+	// <mode> <type> <sha> <space-padded-size>\t<filename>
+	// <mode> <type> <sha>\t<filename>
+
+	var err error
+	posTab := bytes.IndexByte(line, '\t')
+	if posTab == -1 {
+		return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line)
+	}
+
+	entry := new(LsTreeEntry)
+
+	entryAttrs := line[:posTab]
+	entryName := line[posTab+1:]
+
+	entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
+	_ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type
+	entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
+	if len(entryAttrs) > 0 {
+		entrySize := entryAttrs // the last field is the space-padded-size
+		size, _ := strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64)
+		entry.Size = optional.Some(size)
+	}
+
+	switch string(entryMode) {
+	case "100644":
+		entry.EntryMode = EntryModeBlob
+	case "100755":
+		entry.EntryMode = EntryModeExec
+	case "120000":
+		entry.EntryMode = EntryModeSymlink
+	case "160000":
+		entry.EntryMode = EntryModeCommit
+	case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons
+		entry.EntryMode = EntryModeTree
+	default:
+		return nil, fmt.Errorf("unknown type: %v", string(entryMode))
+	}
+
+	entry.ID, err = NewIDFromString(string(entryObjectID))
+	if err != nil {
+		return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err)
+	}
+
+	if len(entryName) > 0 && entryName[0] == '"' {
+		entry.Name, err = strconv.Unquote(string(entryName))
+		if err != nil {
+			return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err)
+		}
+	} else {
+		entry.Name = string(entryName)
+	}
+	return entry, nil
+}
diff --git a/modules/git/parse_nogogit.go b/modules/git/parse_nogogit.go
index 546b38be37..676bb3c76c 100644
--- a/modules/git/parse_nogogit.go
+++ b/modules/git/parse_nogogit.go
@@ -10,8 +10,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"strconv"
-	"strings"
 
 	"code.gitea.io/gitea/modules/log"
 )
@@ -21,71 +19,30 @@ func ParseTreeEntries(data []byte) ([]*TreeEntry, error) {
 	return parseTreeEntries(data, nil)
 }
 
-var sepSpace = []byte{' '}
-
+// parseTreeEntries FIXME this function's design is not right, it should make the caller read all data into memory
 func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
-	var err error
 	entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1)
 	for pos := 0; pos < len(data); {
-		// expect line to be of the form:
-		// <mode> <type> <sha> <space-padded-size>\t<filename>
-		// <mode> <type> <sha>\t<filename>
 		posEnd := bytes.IndexByte(data[pos:], '\n')
 		if posEnd == -1 {
 			posEnd = len(data)
 		} else {
 			posEnd += pos
 		}
+
 		line := data[pos:posEnd]
-		posTab := bytes.IndexByte(line, '\t')
-		if posTab == -1 {
-			return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line)
-		}
-
-		entry := new(TreeEntry)
-		entry.ptree = ptree
-
-		entryAttrs := line[:posTab]
-		entryName := line[posTab+1:]
-
-		entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
-		_ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type
-		entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
-		if len(entryAttrs) > 0 {
-			entrySize := entryAttrs // the last field is the space-padded-size
-			entry.size, _ = strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64)
-			entry.sized = true
-		}
-
-		switch string(entryMode) {
-		case "100644":
-			entry.entryMode = EntryModeBlob
-		case "100755":
-			entry.entryMode = EntryModeExec
-		case "120000":
-			entry.entryMode = EntryModeSymlink
-		case "160000":
-			entry.entryMode = EntryModeCommit
-		case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons
-			entry.entryMode = EntryModeTree
-		default:
-			return nil, fmt.Errorf("unknown type: %v", string(entryMode))
-		}
-
-		entry.ID, err = NewIDFromString(string(entryObjectID))
+		lsTreeLine, err := parseLsTreeLine(line)
 		if err != nil {
-			return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err)
+			return nil, err
 		}
-
-		if len(entryName) > 0 && entryName[0] == '"' {
-			entry.name, err = strconv.Unquote(string(entryName))
-			if err != nil {
-				return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err)
-			}
-		} else {
-			entry.name = string(entryName)
+		entry := &TreeEntry{
+			ptree:     ptree,
+			ID:        lsTreeLine.ID,
+			entryMode: lsTreeLine.EntryMode,
+			name:      lsTreeLine.Name,
+			size:      lsTreeLine.Size.Value(),
+			sized:     lsTreeLine.Size.Has(),
 		}
-
 		pos = posEnd + 1
 		entries = append(entries, entry)
 	}
@@ -100,7 +57,7 @@ func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd *bufio.
 
 loop:
 	for sz > 0 {
-		mode, fname, sha, count, err := ParseTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf)
+		mode, fname, sha, count, err := ParseCatFileTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf)
 		if err != nil {
 			if err == io.EOF {
 				break loop
diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go
index b22805c132..92e35c5a10 100644
--- a/modules/git/pipeline/lfs_nogogit.go
+++ b/modules/git/pipeline/lfs_nogogit.go
@@ -114,7 +114,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
 			case "tree":
 				var n int64
 				for n < size {
-					mode, fname, binObjectID, count, err := git.ParseTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf)
+					mode, fname, binObjectID, count, err := git.ParseCatFileTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf)
 					if err != nil {
 						return nil, err
 					}
diff --git a/modules/git/submodule.go b/modules/git/submodule.go
new file mode 100644
index 0000000000..017b644052
--- /dev/null
+++ b/modules/git/submodule.go
@@ -0,0 +1,66 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"os"
+
+	"code.gitea.io/gitea/modules/log"
+)
+
+type TemplateSubmoduleCommit struct {
+	Path   string
+	Commit string
+}
+
+// GetTemplateSubmoduleCommits returns a list of submodules paths and their commits from a repository
+// This function is only for generating new repos based on existing template, the template couldn't be too large.
+func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submoduleCommits []TemplateSubmoduleCommit, _ error) {
+	stdoutReader, stdoutWriter, err := os.Pipe()
+	if err != nil {
+		return nil, err
+	}
+	opts := &RunOpts{
+		Dir:    repoPath,
+		Stdout: stdoutWriter,
+		PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
+			_ = stdoutWriter.Close()
+			defer stdoutReader.Close()
+
+			scanner := bufio.NewScanner(stdoutReader)
+			for scanner.Scan() {
+				entry, err := parseLsTreeLine(scanner.Bytes())
+				if err != nil {
+					cancel()
+					return err
+				}
+				if entry.EntryMode == EntryModeCommit {
+					submoduleCommits = append(submoduleCommits, TemplateSubmoduleCommit{Path: entry.Name, Commit: entry.ID.String()})
+				}
+			}
+			return scanner.Err()
+		},
+	}
+	err = NewCommand(ctx, "ls-tree", "-r", "--", "HEAD").Run(opts)
+	if err != nil {
+		return nil, fmt.Errorf("GetTemplateSubmoduleCommits: error running git ls-tree: %v", err)
+	}
+	return submoduleCommits, nil
+}
+
+// AddTemplateSubmoduleIndexes Adds the given submodules to the git index.
+// It is only for generating new repos based on existing template, requires the .gitmodules file to be already present in the work dir.
+func AddTemplateSubmoduleIndexes(ctx context.Context, repoPath string, submodules []TemplateSubmoduleCommit) error {
+	for _, submodule := range submodules {
+		cmd := NewCommand(ctx, "update-index", "--add", "--cacheinfo", "160000").AddDynamicArguments(submodule.Commit, submodule.Path)
+		if stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}); err != nil {
+			log.Error("Unable to add %s as submodule to repo %s: stdout %s\nError: %v", submodule.Path, repoPath, stdout, err)
+			return err
+		}
+	}
+	return nil
+}
diff --git a/modules/git/submodule_test.go b/modules/git/submodule_test.go
new file mode 100644
index 0000000000..d53946a27d
--- /dev/null
+++ b/modules/git/submodule_test.go
@@ -0,0 +1,48 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"context"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetTemplateSubmoduleCommits(t *testing.T) {
+	testRepoPath := filepath.Join(testReposDir, "repo4_submodules")
+	submodules, err := GetTemplateSubmoduleCommits(DefaultContext, testRepoPath)
+	require.NoError(t, err)
+
+	assert.Len(t, submodules, 2)
+
+	assert.EqualValues(t, "<°)))><", submodules[0].Path)
+	assert.EqualValues(t, "d2932de67963f23d43e1c7ecf20173e92ee6c43c", submodules[0].Commit)
+
+	assert.EqualValues(t, "libtest", submodules[1].Path)
+	assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[1].Commit)
+}
+
+func TestAddTemplateSubmoduleIndexes(t *testing.T) {
+	ctx := context.Background()
+	tmpDir := t.TempDir()
+	var err error
+	_, _, err = NewCommand(ctx, "init").RunStdString(&RunOpts{Dir: tmpDir})
+	require.NoError(t, err)
+	_ = os.Mkdir(filepath.Join(tmpDir, "new-dir"), 0o755)
+	err = AddTemplateSubmoduleIndexes(ctx, tmpDir, []TemplateSubmoduleCommit{{Path: "new-dir", Commit: "1234567890123456789012345678901234567890"}})
+	require.NoError(t, err)
+	_, _, err = NewCommand(ctx, "add", "--all").RunStdString(&RunOpts{Dir: tmpDir})
+	require.NoError(t, err)
+	_, _, err = NewCommand(ctx, "-c", "user.name=a", "-c", "user.email=b", "commit", "-m=test").RunStdString(&RunOpts{Dir: tmpDir})
+	require.NoError(t, err)
+	submodules, err := GetTemplateSubmoduleCommits(DefaultContext, tmpDir)
+	require.NoError(t, err)
+	assert.Len(t, submodules, 1)
+	assert.EqualValues(t, "new-dir", submodules[0].Path)
+	assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[0].Commit)
+}
diff --git a/modules/git/tests/repos/repo4_submodules/HEAD b/modules/git/tests/repos/repo4_submodules/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/modules/git/tests/repos/repo4_submodules/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/modules/git/tests/repos/repo4_submodules/config b/modules/git/tests/repos/repo4_submodules/config
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/modules/git/tests/repos/repo4_submodules/config
@@ -0,0 +1,4 @@
+[core]
+	repositoryformatversion = 0
+	filemode = true
+	bare = true
diff --git a/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 b/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74
new file mode 100644
index 0000000000000000000000000000000000000000..7596090b49fc8304d4c5599e09fa97e65fd86ceb
GIT binary patch
literal 110
zcmV-!0FnQA0V^p=O;s>7G+;0^FfcPQQP4}zEXmDJDa}bOW;p&J<*nxySLM?ymfqiE
ze(9X_7FiQRGXo${usO6rQ&ZE<hT+m=-Dj1_pKP5U9)I(RvG}Flvm-W8H946{C8@<F
Q3_>Ph6%)`10HMe&nHoGW2LJ#7

literal 0
HcmV?d00001

diff --git a/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 b/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34
new file mode 100644
index 0000000000000000000000000000000000000000..e3a13c156dce4d306921d33f4414867aea4b58c0
GIT binary patch
literal 112
zcmV-$0FVE80ZYosPf{>6HDQP@E=|hKPbtkwRZz;wOe#q&E>Vi*;w(rk$xyIWfQoQ&
zmKNmzxfvxT1;tkS`Z@W@i8&eh#U=Vs1$yb3C0xix*&N!Ssi|pa12jtk3ZO>9WZ>cu
SqxDO23-n=fVB-M&&@!0EvNJaT

literal 0
HcmV?d00001

diff --git a/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 b/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437
new file mode 100644
index 0000000000..a8d6e5c17c
--- /dev/null
+++ b/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437
@@ -0,0 +1,2 @@
+x��[
+�0E��*�_��$M�5tifBk Iŕ�7�k~��9ܘ��ܠ���.j��	�O���"z�`�#I�irF��͹��$%����|4)��?t��=��:K��#[$D����^�����Ӓy�HU/�f?G
\ No newline at end of file
diff --git a/modules/git/tests/repos/repo4_submodules/refs/heads/master b/modules/git/tests/repos/repo4_submodules/refs/heads/master
new file mode 100644
index 0000000000..102bc34da8
--- /dev/null
+++ b/modules/git/tests/repos/repo4_submodules/refs/heads/master
@@ -0,0 +1 @@
+e1e59caba97193d48862d6809912043871f37437
diff --git a/modules/git/tree.go b/modules/git/tree.go
index d35dc58d8d..5a644f6c87 100644
--- a/modules/git/tree.go
+++ b/modules/git/tree.go
@@ -17,7 +17,7 @@ func NewTree(repo *Repository, id ObjectID) *Tree {
 	}
 }
 
-// SubTree get a sub tree by the sub dir path
+// SubTree get a subtree by the sub dir path
 func (t *Tree) SubTree(rpath string) (*Tree, error) {
 	if len(rpath) == 0 {
 		return t, nil
@@ -63,7 +63,7 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error
 	return filelist, err
 }
 
-// GetTreePathLatestCommitID returns the latest commit of a tree path
+// GetTreePathLatestCommit returns the latest commit of a tree path
 func (repo *Repository) GetTreePathLatestCommit(refName, treePath string) (*Commit, error) {
 	stdout, _, err := NewCommand(repo.Ctx, "rev-list", "-1").
 		AddDynamicArguments(refName).AddDashesAndList(treePath).
diff --git a/modules/git/tree_blob_nogogit.go b/modules/git/tree_blob_nogogit.go
index 92d3d107a7..b7bcf40edd 100644
--- a/modules/git/tree_blob_nogogit.go
+++ b/modules/git/tree_blob_nogogit.go
@@ -17,7 +17,6 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
 			ptree:     t,
 			ID:        t.ID,
 			name:      "",
-			fullName:  "",
 			entryMode: EntryModeTree,
 		}, nil
 	}
diff --git a/modules/git/tree_entry_nogogit.go b/modules/git/tree_entry_nogogit.go
index 1c3bcd197a..81fb638d56 100644
--- a/modules/git/tree_entry_nogogit.go
+++ b/modules/git/tree_entry_nogogit.go
@@ -9,23 +9,17 @@ import "code.gitea.io/gitea/modules/log"
 
 // TreeEntry the leaf in the git tree
 type TreeEntry struct {
-	ID ObjectID
-
+	ID    ObjectID
 	ptree *Tree
 
 	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
 }
 
diff --git a/services/repository/generate.go b/services/repository/generate.go
index f2280de8b2..d3b750f7d4 100644
--- a/services/repository/generate.go
+++ b/services/repository/generate.go
@@ -9,7 +9,6 @@ import (
 	"context"
 	"fmt"
 	"os"
-	"path"
 	"path/filepath"
 	"regexp"
 	"strconv"
@@ -123,7 +122,7 @@ func (gt *GiteaTemplate) Globs() []glob.Glob {
 	return gt.globs
 }
 
-func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
+func readGiteaTemplateFile(tmpDir string) (*GiteaTemplate, error) {
 	gtPath := filepath.Join(tmpDir, ".gitea", "template")
 	if _, err := os.Stat(gtPath); os.IsNotExist(err) {
 		return nil, nil
@@ -136,12 +135,55 @@ func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
 		return nil, err
 	}
 
-	gt := &GiteaTemplate{
-		Path:    gtPath,
-		Content: content,
-	}
+	return &GiteaTemplate{Path: gtPath, Content: content}, nil
+}
 
-	return gt, nil
+func processGiteaTemplateFile(tmpDir string, templateRepo, generateRepo *repo_model.Repository, giteaTemplateFile *GiteaTemplate) error {
+	if err := util.Remove(giteaTemplateFile.Path); err != nil {
+		return fmt.Errorf("remove .giteatemplate: %w", err)
+	}
+	if len(giteaTemplateFile.Globs()) == 0 {
+		return nil // Avoid walking tree if there are no globs
+	}
+	tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
+	return filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error {
+		if walkErr != nil {
+			return walkErr
+		}
+
+		if d.IsDir() {
+			return nil
+		}
+
+		base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
+		for _, g := range giteaTemplateFile.Globs() {
+			if g.Match(base) {
+				content, err := os.ReadFile(path)
+				if err != nil {
+					return err
+				}
+
+				generatedContent := []byte(generateExpansion(string(content), templateRepo, generateRepo, false))
+				if err := os.WriteFile(path, generatedContent, 0o644); err != nil {
+					return err
+				}
+
+				substPath := filepath.FromSlash(filepath.Join(tmpDirSlash, generateExpansion(base, templateRepo, generateRepo, true)))
+
+				// Create parent subdirectories if needed or continue silently if it exists
+				if err = os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil {
+					return err
+				}
+
+				// Substitute filename variables
+				if err = os.Rename(path, substPath); err != nil {
+					return err
+				}
+				break
+			}
+		}
+		return nil
+	}) // end: WalkDir
 }
 
 func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository, tmpDir string) error {
@@ -167,71 +209,30 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
 		return fmt.Errorf("git clone: %w", err)
 	}
 
-	if err := util.RemoveAll(path.Join(tmpDir, ".git")); err != nil {
+	// Get active submodules from the template
+	submodules, err := git.GetTemplateSubmoduleCommits(ctx, tmpDir)
+	if err != nil {
+		return fmt.Errorf("GetTemplateSubmoduleCommits: %w", err)
+	}
+
+	if err = util.RemoveAll(filepath.Join(tmpDir, ".git")); err != nil {
 		return fmt.Errorf("remove git dir: %w", err)
 	}
 
 	// Variable expansion
-	gt, err := checkGiteaTemplate(tmpDir)
+	giteaTemplateFile, err := readGiteaTemplateFile(tmpDir)
 	if err != nil {
-		return fmt.Errorf("checkGiteaTemplate: %w", err)
+		return fmt.Errorf("readGiteaTemplateFile: %w", err)
 	}
 
-	if gt != nil {
-		if err := util.Remove(gt.Path); err != nil {
-			return fmt.Errorf("remove .giteatemplate: %w", err)
-		}
-
-		// Avoid walking tree if there are no globs
-		if len(gt.Globs()) > 0 {
-			tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
-			if err := filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error {
-				if walkErr != nil {
-					return walkErr
-				}
-
-				if d.IsDir() {
-					return nil
-				}
-
-				base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
-				for _, g := range gt.Globs() {
-					if g.Match(base) {
-						content, err := os.ReadFile(path)
-						if err != nil {
-							return err
-						}
-
-						if err := os.WriteFile(path,
-							[]byte(generateExpansion(string(content), templateRepo, generateRepo, false)),
-							0o644); err != nil {
-							return err
-						}
-
-						substPath := filepath.FromSlash(filepath.Join(tmpDirSlash,
-							generateExpansion(base, templateRepo, generateRepo, true)))
-
-						// Create parent subdirectories if needed or continue silently if it exists
-						if err := os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil {
-							return err
-						}
-
-						// Substitute filename variables
-						if err := os.Rename(path, substPath); err != nil {
-							return err
-						}
-
-						break
-					}
-				}
-				return nil
-			}); err != nil {
-				return err
-			}
+	if giteaTemplateFile != nil {
+		err = processGiteaTemplateFile(tmpDir, templateRepo, generateRepo, giteaTemplateFile)
+		if err != nil {
+			return err
 		}
 	}
 
-	if err := git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil {
+	if err = git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil {
 		return err
 	}
 
@@ -243,6 +244,10 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
 		return fmt.Errorf("git remote add: %w", err)
 	}
 
+	if err = git.AddTemplateSubmoduleIndexes(ctx, tmpDir, submodules); err != nil {
+		return fmt.Errorf("failed to add submodules: %v", err)
+	}
+
 	// set default branch based on whether it's specified in the newly generated repo or not
 	defaultBranch := repo.DefaultBranch
 	if strings.TrimSpace(defaultBranch) == "" {
diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl
index 2d555e4c2e..c50a0d312f 100644
--- a/templates/repo/view_list.tmpl
+++ b/templates/repo/view_list.tmpl
@@ -19,9 +19,9 @@
 					{{svg "octicon-file-submodule"}}
 					{{$refURL := $subModuleFile.RefURL AppUrl $.Repository.FullName $.SSHDomain}} {{/* FIXME: the usage of AppUrl seems incorrect, it would be fixed in the future, use AppSubUrl instead */}}
 					{{if $refURL}}
-						<a class="muted" href="{{$refURL}}">{{$entry.Name}}</a><span class="at">@</span><a href="{{$refURL}}/commit/{{PathEscape $subModuleFile.RefID}}">{{ShortSha $subModuleFile.RefID}}</a>
+						<a class="muted" href="{{$refURL}}">{{$entry.Name}}</a> <span class="at">@</span> <a href="{{$refURL}}/commit/{{PathEscape $subModuleFile.RefID}}">{{ShortSha $subModuleFile.RefID}}</a>
 					{{else}}
-						{{$entry.Name}}<span class="at">@</span>{{ShortSha $subModuleFile.RefID}}
+						{{$entry.Name}} <span class="at">@</span> {{ShortSha $subModuleFile.RefID}}
 					{{end}}
 				{{else}}
 					{{if $entry.IsDir}}