Skip to content

Inherit submodules from template repository content #16237

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions modules/git/batch_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,15 +242,15 @@ 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
//
// Each line is composed of:
// <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
Expand All @@ -260,7 +260,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{}
}

Expand Down
113 changes: 62 additions & 51 deletions modules/git/parse_nogogit.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,68 +23,79 @@ func ParseTreeEntries(data []byte) ([]*TreeEntry, error) {

var sepSpace = []byte{' '}

func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
func parseLsTreeLine(line []byte) (*TreeEntry, 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(TreeEntry)

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 {
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
}

// 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) {
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))
line := data[pos:posEnd]
entry, err := parseLsTreeLine(line)
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 nil, err
}
entry.ptree = ptree

pos = posEnd + 1
entries = append(entries, entry)
Expand All @@ -100,7 +111,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
Expand Down
2 changes: 1 addition & 1 deletion modules/git/pipeline/lfs_nogogit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
63 changes: 63 additions & 0 deletions modules/git/submodule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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())

Check failure on line 33 in modules/git/submodule.go

View workflow job for this annotation

GitHub Actions / backend

undefined: parseLsTreeLine

Check failure on line 33 in modules/git/submodule.go

View workflow job for this annotation

GitHub Actions / test-sqlite

undefined: parseLsTreeLine
if err != nil {
cancel()
return err
}
if entry.IsSubModule() {
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
}
23 changes: 23 additions & 0 deletions modules/git/submodule_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package git

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestRepository_GetSubmoduleCommits(t *testing.T) {
testRepoPath := filepath.Join(testReposDir, "repo4_submodules")
submodules, err := GetTemplateSubmoduleCommits(DefaultContext, testRepoPath)
require.NoError(t, err)

assert.EqualValues(t, len(submodules), 2)

Check failure on line 16 in modules/git/submodule_test.go

View workflow job for this annotation

GitHub Actions / lint-backend

len: use assert.Len (testifylint)

Check failure on line 16 in modules/git/submodule_test.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

len: use assert.Len (testifylint)

Check failure on line 16 in modules/git/submodule_test.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

len: use assert.Len (testifylint)

assert.EqualValues(t, submodules[0].Path, "<°)))><")

Check failure on line 18 in modules/git/submodule_test.go

View workflow job for this annotation

GitHub Actions / lint-backend

expected-actual: need to reverse actual and expected values (testifylint)

Check failure on line 18 in modules/git/submodule_test.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

expected-actual: need to reverse actual and expected values (testifylint)

Check failure on line 18 in modules/git/submodule_test.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

expected-actual: need to reverse actual and expected values (testifylint)
assert.EqualValues(t, submodules[0].Commit, "d2932de67963f23d43e1c7ecf20173e92ee6c43c")

Check failure on line 19 in modules/git/submodule_test.go

View workflow job for this annotation

GitHub Actions / lint-backend

expected-actual: need to reverse actual and expected values (testifylint)

Check failure on line 19 in modules/git/submodule_test.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

expected-actual: need to reverse actual and expected values (testifylint)

Check failure on line 19 in modules/git/submodule_test.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

expected-actual: need to reverse actual and expected values (testifylint)

assert.EqualValues(t, submodules[1].Path, "libtest")

Check failure on line 21 in modules/git/submodule_test.go

View workflow job for this annotation

GitHub Actions / lint-backend

expected-actual: need to reverse actual and expected values (testifylint)

Check failure on line 21 in modules/git/submodule_test.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

expected-actual: need to reverse actual and expected values (testifylint)

Check failure on line 21 in modules/git/submodule_test.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

expected-actual: need to reverse actual and expected values (testifylint)
assert.EqualValues(t, submodules[1].Commit, "1234567890123456789012345678901234567890")

Check failure on line 22 in modules/git/submodule_test.go

View workflow job for this annotation

GitHub Actions / lint-backend

expected-actual: need to reverse actual and expected values (testifylint)

Check failure on line 22 in modules/git/submodule_test.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

expected-actual: need to reverse actual and expected values (testifylint)

Check failure on line 22 in modules/git/submodule_test.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

expected-actual: need to reverse actual and expected values (testifylint)
}
1 change: 1 addition & 0 deletions modules/git/tests/repos/repo4_submodules/HEAD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ref: refs/heads/master
4 changes: 4 additions & 0 deletions modules/git/tests/repos/repo4_submodules/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[core]
repositoryformatversion = 0
filemode = true
bare = true
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions modules/git/tests/repos/repo4_submodules/refs/heads/master
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
e1e59caba97193d48862d6809912043871f37437
4 changes: 2 additions & 2 deletions modules/git/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down
Loading
Loading