mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-04 05:18:25 +00:00 
			
		
		
		
	Backport #16237 (it more likely a bug fix) Co-authored-by: Steffen Schröter <steffen@vexar.de>
This commit is contained in:
		@@ -253,7 +253,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte {
 | 
				
			|||||||
	return out
 | 
						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.
 | 
					// 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
 | 
					// 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>
 | 
					// <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
 | 
					// 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
 | 
						var readBytes []byte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Read the Mode & fname
 | 
						// Read the Mode & fname
 | 
				
			||||||
@@ -271,7 +271,7 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	idx := bytes.IndexByte(readBytes, ' ')
 | 
						idx := bytes.IndexByte(readBytes, ' ')
 | 
				
			||||||
	if idx < 0 {
 | 
						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{}
 | 
							return mode, fname, sha, n, &ErrNotExist{}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										78
									
								
								modules/git/parse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								modules/git/parse.go
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -10,8 +10,6 @@ import (
 | 
				
			|||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -21,71 +19,30 @@ func ParseTreeEntries(data []byte) ([]*TreeEntry, error) {
 | 
				
			|||||||
	return parseTreeEntries(data, nil)
 | 
						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) {
 | 
					func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
 | 
				
			||||||
	var err error
 | 
					 | 
				
			||||||
	entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1)
 | 
						entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1)
 | 
				
			||||||
	for pos := 0; pos < len(data); {
 | 
						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')
 | 
							posEnd := bytes.IndexByte(data[pos:], '\n')
 | 
				
			||||||
		if posEnd == -1 {
 | 
							if posEnd == -1 {
 | 
				
			||||||
			posEnd = len(data)
 | 
								posEnd = len(data)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			posEnd += pos
 | 
								posEnd += pos
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		line := data[pos:posEnd]
 | 
							line := data[pos:posEnd]
 | 
				
			||||||
		posTab := bytes.IndexByte(line, '\t')
 | 
							lsTreeLine, err := parseLsTreeLine(line)
 | 
				
			||||||
		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))
 | 
					 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err)
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							entry := &TreeEntry{
 | 
				
			||||||
		if len(entryName) > 0 && entryName[0] == '"' {
 | 
								ptree:     ptree,
 | 
				
			||||||
			entry.name, err = strconv.Unquote(string(entryName))
 | 
								ID:        lsTreeLine.ID,
 | 
				
			||||||
			if err != nil {
 | 
								entryMode: lsTreeLine.EntryMode,
 | 
				
			||||||
				return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err)
 | 
								name:      lsTreeLine.Name,
 | 
				
			||||||
			}
 | 
								size:      lsTreeLine.Size.Value(),
 | 
				
			||||||
		} else {
 | 
								sized:     lsTreeLine.Size.Has(),
 | 
				
			||||||
			entry.name = string(entryName)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					 | 
				
			||||||
		pos = posEnd + 1
 | 
							pos = posEnd + 1
 | 
				
			||||||
		entries = append(entries, entry)
 | 
							entries = append(entries, entry)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -100,7 +57,7 @@ func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd *bufio.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
loop:
 | 
					loop:
 | 
				
			||||||
	for sz > 0 {
 | 
						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 != nil {
 | 
				
			||||||
			if err == io.EOF {
 | 
								if err == io.EOF {
 | 
				
			||||||
				break loop
 | 
									break loop
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -114,7 +114,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
 | 
				
			|||||||
			case "tree":
 | 
								case "tree":
 | 
				
			||||||
				var n int64
 | 
									var n int64
 | 
				
			||||||
				for n < size {
 | 
									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 {
 | 
										if err != nil {
 | 
				
			||||||
						return nil, err
 | 
											return nil, err
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										66
									
								
								modules/git/submodule.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								modules/git/submodule.go
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										48
									
								
								modules/git/submodule_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								modules/git/submodule_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								modules/git/tests/repos/repo4_submodules/HEAD
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								modules/git/tests/repos/repo4_submodules/HEAD
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					ref: refs/heads/master
 | 
				
			||||||
							
								
								
									
										4
									
								
								modules/git/tests/repos/repo4_submodules/config
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								modules/git/tests/repos/repo4_submodules/config
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					[core]
 | 
				
			||||||
 | 
						repositoryformatversion = 0
 | 
				
			||||||
 | 
						filemode = true
 | 
				
			||||||
 | 
						bare = true
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					x<01><>[
 | 
				
			||||||
 | 
					<EFBFBD>0E<><45>*<2A>_<EFBFBD><5F>$M<10>5tifBk Iŕ<49>7<>k~<7E><>9ܘ<39><DC98>ܠ<1B><><11>.j<><6A>	<09>O<><0C><>"z<>`<60>#I<>irF<72><46><EFBFBD><CDB9>$%<25><><18><>|4)<29><>?t<><74>=<3D><1D>:K<><4B><EFBFBD>#[$D<15><><1F><>^<5E><><EFBFBD><EFBFBD><EFBFBD>Ӓy<D392>HU/<2F>f?G
 | 
				
			||||||
@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					e1e59caba97193d48862d6809912043871f37437
 | 
				
			||||||
@@ -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) {
 | 
					func (t *Tree) SubTree(rpath string) (*Tree, error) {
 | 
				
			||||||
	if len(rpath) == 0 {
 | 
						if len(rpath) == 0 {
 | 
				
			||||||
		return t, nil
 | 
							return t, nil
 | 
				
			||||||
@@ -63,7 +63,7 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error
 | 
				
			|||||||
	return filelist, err
 | 
						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) {
 | 
					func (repo *Repository) GetTreePathLatestCommit(refName, treePath string) (*Commit, error) {
 | 
				
			||||||
	stdout, _, err := NewCommand(repo.Ctx, "rev-list", "-1").
 | 
						stdout, _, err := NewCommand(repo.Ctx, "rev-list", "-1").
 | 
				
			||||||
		AddDynamicArguments(refName).AddDashesAndList(treePath).
 | 
							AddDynamicArguments(refName).AddDashesAndList(treePath).
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,23 +9,17 @@ import "code.gitea.io/gitea/modules/log"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// TreeEntry the leaf in the git tree
 | 
					// TreeEntry the leaf in the git tree
 | 
				
			||||||
type TreeEntry struct {
 | 
					type TreeEntry struct {
 | 
				
			||||||
	ID ObjectID
 | 
						ID    ObjectID
 | 
				
			||||||
 | 
					 | 
				
			||||||
	ptree *Tree
 | 
						ptree *Tree
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,6 @@ import (
 | 
				
			|||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path"
 | 
					 | 
				
			||||||
	"path/filepath"
 | 
						"path/filepath"
 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
@@ -123,7 +122,7 @@ func (gt *GiteaTemplate) Globs() []glob.Glob {
 | 
				
			|||||||
	return gt.globs
 | 
						return gt.globs
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
 | 
					func readGiteaTemplateFile(tmpDir string) (*GiteaTemplate, error) {
 | 
				
			||||||
	gtPath := filepath.Join(tmpDir, ".gitea", "template")
 | 
						gtPath := filepath.Join(tmpDir, ".gitea", "template")
 | 
				
			||||||
	if _, err := os.Stat(gtPath); os.IsNotExist(err) {
 | 
						if _, err := os.Stat(gtPath); os.IsNotExist(err) {
 | 
				
			||||||
		return nil, nil
 | 
							return nil, nil
 | 
				
			||||||
@@ -136,12 +135,55 @@ func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
 | 
				
			|||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	gt := &GiteaTemplate{
 | 
						return &GiteaTemplate{Path: gtPath, Content: content}, nil
 | 
				
			||||||
		Path:    gtPath,
 | 
					}
 | 
				
			||||||
		Content: content,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	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 {
 | 
					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)
 | 
							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)
 | 
							return fmt.Errorf("remove git dir: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Variable expansion
 | 
						// Variable expansion
 | 
				
			||||||
	gt, err := checkGiteaTemplate(tmpDir)
 | 
						giteaTemplateFile, err := readGiteaTemplateFile(tmpDir)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("checkGiteaTemplate: %w", err)
 | 
							return fmt.Errorf("readGiteaTemplateFile: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if gt != nil {
 | 
						if giteaTemplateFile != nil {
 | 
				
			||||||
		if err := util.Remove(gt.Path); err != nil {
 | 
							err = processGiteaTemplateFile(tmpDir, templateRepo, generateRepo, giteaTemplateFile)
 | 
				
			||||||
			return fmt.Errorf("remove .giteatemplate: %w", err)
 | 
							if err != nil {
 | 
				
			||||||
		}
 | 
								return 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 err := git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil {
 | 
						if err = git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -243,6 +244,10 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
 | 
				
			|||||||
		return fmt.Errorf("git remote add: %w", err)
 | 
							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
 | 
						// set default branch based on whether it's specified in the newly generated repo or not
 | 
				
			||||||
	defaultBranch := repo.DefaultBranch
 | 
						defaultBranch := repo.DefaultBranch
 | 
				
			||||||
	if strings.TrimSpace(defaultBranch) == "" {
 | 
						if strings.TrimSpace(defaultBranch) == "" {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,9 +19,9 @@
 | 
				
			|||||||
					{{svg "octicon-file-submodule"}}
 | 
										{{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 */}}
 | 
										{{$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}}
 | 
										{{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}}
 | 
										{{else}}
 | 
				
			||||||
						{{$entry.Name}}<span class="at">@</span>{{ShortSha $subModuleFile.RefID}}
 | 
											{{$entry.Name}} <span class="at">@</span> {{ShortSha $subModuleFile.RefID}}
 | 
				
			||||||
					{{end}}
 | 
										{{end}}
 | 
				
			||||||
				{{else}}
 | 
									{{else}}
 | 
				
			||||||
					{{if $entry.IsDir}}
 | 
										{{if $entry.IsDir}}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user