Add go wrapper around git diff-tree --raw -r -M

* Implemented calling git diff-tree
 * Ensures wrapper function is called with valid arguments
 * Parses output into go struct, using strong typing when possible
 * Modifies services/gitdiff/testdata/acedemic-module
  * Makes it a bare repo
  * Adds a branch which updates readme
  * Adds a branch which updates the webpack config
pull/33369/head
Alexander McRae 1 week ago
parent 77d14fb6d3
commit a02be75be6
  1. 16
      modules/git/parse.go
  2. 27
      modules/git/tree_entry_mode.go
  3. 229
      services/gitdiff/git_diff_tree.go
  4. 424
      services/gitdiff/git_diff_tree_test.go
  5. 2
      services/gitdiff/testdata/academic-module/config
  6. 1
      services/gitdiff/testdata/academic-module/logs/HEAD
  7. 1
      services/gitdiff/testdata/academic-module/logs/refs/heads/master
  8. 1
      services/gitdiff/testdata/academic-module/logs/refs/heads/update-readme
  9. 2
      services/gitdiff/testdata/academic-module/objects/34/4e0ca8aa791cc4164fb0ea645f334fd40d00f0
  10. BIN
      services/gitdiff/testdata/academic-module/objects/52/8846b39d8f7e68e8081d586ea3e94be23afa7f
  11. BIN
      services/gitdiff/testdata/academic-module/objects/64/ba8d2d41f195318e69793debe5abca244a26a3
  12. BIN
      services/gitdiff/testdata/academic-module/objects/81/9dc756646ffd0730a163b5a8da65b84a6d504e
  13. BIN
      services/gitdiff/testdata/academic-module/objects/ca/07ea08ae0fa243d6e0a4129843c5a25a02f499
  14. BIN
      services/gitdiff/testdata/academic-module/objects/cb/054bf7bad450a8602dec25e6fc9925a6d84332
  15. 2
      services/gitdiff/testdata/academic-module/refs/heads/master
  16. 1
      services/gitdiff/testdata/academic-module/refs/heads/update-readme

@ -46,19 +46,9 @@ func parseLsTreeLine(line []byte) (*LsTreeEntry, error) {
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.EntryMode, err = ParseEntryMode(string(entryMode))
if err != nil || entry.EntryMode == EntryModeNoEntry {
return nil, fmt.Errorf("invalid ls-tree output (invalid mode): %q, err: %w", line, err)
}
entry.ID, err = NewIDFromString(string(entryObjectID))

@ -3,7 +3,10 @@
package git
import "strconv"
import (
"fmt"
"strconv"
)
// EntryMode the type of the object in the git tree
type EntryMode int
@ -11,6 +14,9 @@ type EntryMode int
// There are only a few file modes in Git. They look like unix file modes, but they can only be
// one of these.
const (
// EntryModeNoEntry is possible if the file was added or removed in a commit. In the case of
// added the base commit will not have the file in its tree so a mode of 0o000000 is used.
EntryModeNoEntry EntryMode = 0o000000
// EntryModeBlob
EntryModeBlob EntryMode = 0o100644
// EntryModeExec
@ -33,3 +39,22 @@ func ToEntryMode(value string) EntryMode {
v, _ := strconv.ParseInt(value, 8, 32)
return EntryMode(v)
}
func ParseEntryMode(mode string) (EntryMode, error) {
switch mode {
case "000000":
return EntryModeNoEntry, nil
case "100644":
return EntryModeBlob, nil
case "100755":
return EntryModeExec, nil
case "120000":
return EntryModeSymlink, nil
case "160000":
return EntryModeCommit, nil
case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons
return EntryModeTree, nil
default:
return 0, fmt.Errorf("unparsable entry mode: %s", mode)
}
}

@ -0,0 +1,229 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitdiff
import (
"bufio"
"context"
"fmt"
"io"
"strings"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
)
type DiffTree struct {
Files []*DiffTreeRecord
}
type DiffTreeRecord struct {
// Status is one of 'added', 'deleted', 'modified', 'renamed', 'copied', 'typechanged', 'unmerged', 'unknown'
Status string
HeadPath string
BasePath string
HeadMode git.EntryMode
BaseMode git.EntryMode
HeadBlobID string
BaseBlobID string
}
// GetDiffTree returns the list of path of the files that have changed between the two commits.
// If useMergeBase is true, the diff will be calculated using the merge base of the two commits.
// This is the same behavior as using a three-dot diff in git diff.
func GetDiffTree(ctx context.Context, gitRepo *git.Repository, useMergeBase bool, baseSha, headSha string) (*DiffTree, error) {
gitDiffTreeRecords, err := runGitDiffTree(ctx, gitRepo, useMergeBase, baseSha, headSha)
if err != nil {
return nil, err
}
return &DiffTree{
Files: gitDiffTreeRecords,
}, nil
}
func runGitDiffTree(ctx context.Context, gitRepo *git.Repository, useMergeBase bool, baseSha, headSha string) ([]*DiffTreeRecord, error) {
useMergeBase, baseCommitID, headCommitID, err := validateGitDiffTreeArguments(gitRepo, useMergeBase, baseSha, headSha)
if err != nil {
return nil, err
}
cmd := git.NewCommand(ctx, "diff-tree", "--raw", "-r", "--find-renames", "--root")
if useMergeBase {
cmd.AddArguments("--merge-base")
}
cmd.AddDynamicArguments(baseCommitID, headCommitID)
stdout, _, runErr := cmd.RunStdString(&git.RunOpts{Dir: gitRepo.Path})
if runErr != nil {
log.Warn("git diff-tree: %v", runErr)
return nil, runErr
}
return parseGitDiffTree(strings.NewReader(stdout))
}
func validateGitDiffTreeArguments(gitRepo *git.Repository, useMergeBase bool, baseSha, headSha string) (bool, string, string, error) {
// if the head is empty its an error
if headSha == "" {
return false, "", "", fmt.Errorf("headSha is empty")
}
// if the head commit doesn't exist its and error
headCommit, err := gitRepo.GetCommit(headSha)
if err != nil {
return false, "", "", fmt.Errorf("failed to get commit headSha: %v", err)
}
headCommitID := headCommit.ID.String()
// if the base is empty we should use the parent of the head commit
if baseSha == "" {
// if the headCommit has no parent we should use an empty commit
// this can happen when we are generating a diff against an orphaned commit
if headCommit.ParentCount() == 0 {
objectFormat, err := gitRepo.GetObjectFormat()
if err != nil {
return false, "", "", err
}
// We set use merge base to false because we have no base commit
return false, objectFormat.EmptyTree().String(), headCommitID, nil
}
baseCommit, err := headCommit.Parent(0)
if err != nil {
return false, "", "", fmt.Errorf("baseSha is '', attempted to use parent of commit %s, got error: %v", headCommit.ID.String(), err)
}
return useMergeBase, baseCommit.ID.String(), headCommitID, nil
}
// try and get the base commit
baseCommit, err := gitRepo.GetCommit(baseSha)
// propagate the error if we couldn't get the base commit
if err != nil {
return useMergeBase, "", "", fmt.Errorf("failed to get base commit %s: %v", baseSha, err)
}
return useMergeBase, baseCommit.ID.String(), headCommit.ID.String(), nil
}
func parseGitDiffTree(gitOutput io.Reader) ([]*DiffTreeRecord, error) {
/*
The output of `git diff-tree --raw -r --find-renames` is of the form:
:<old_mode> <new_mode> <old_sha> <new_sha> <status>\t<path>
or for renames:
:<old_mode> <new_mode> <old_sha> <new_sha> <status>\t<old_path>\t<new_path>
See: <https://git-scm.com/docs/git-diff-tree#_raw_output_format> for more details
*/
results := make([]*DiffTreeRecord, 0)
lines := bufio.NewScanner(gitOutput)
for lines.Scan() {
line := lines.Text()
if len(line) == 0 {
continue
}
record, err := parseGitDiffTreeLine(line)
if err != nil {
return nil, err
}
results = append(results, record)
}
if err := lines.Err(); err != nil {
return nil, err
}
return results, nil
}
func parseGitDiffTreeLine(line string) (*DiffTreeRecord, error) {
line = strings.TrimPrefix(line, ":")
splitSections := strings.SplitN(line, "\t", 2)
if len(splitSections) < 2 {
return nil, fmt.Errorf("unparsable output for diff --raw: `%s`)", line)
}
fields := strings.Fields(splitSections[0])
if len(fields) < 5 {
return nil, fmt.Errorf("unparsable output for diff --raw: `%s`, expected 5 space delimited values got %d)", line, len(fields))
}
baseMode, err := git.ParseEntryMode(fields[0])
if err != nil {
return nil, err
}
headMode, err := git.ParseEntryMode(fields[1])
if err != nil {
return nil, err
}
baseBlobID := fields[2]
headBlobID := fields[3]
status, err := statusFromLetter(fields[4])
if err != nil {
return nil, err
}
filePaths := strings.Split(splitSections[1], "\t")
var headPath, basePath string
if status == "renamed" {
if len(filePaths) != 2 {
return nil, fmt.Errorf("unparsable output for diff --raw: `%s`, expected 2 paths found %d", line, len(filePaths))
}
basePath = filePaths[0]
headPath = filePaths[1]
} else {
basePath = filePaths[0]
headPath = filePaths[0]
}
return &DiffTreeRecord{
Status: status,
BaseMode: baseMode,
HeadMode: headMode,
BaseBlobID: baseBlobID,
HeadBlobID: headBlobID,
BasePath: basePath,
HeadPath: headPath,
}, nil
}
func statusFromLetter(letter string) (string, error) {
if len(letter) < 1 {
return "", fmt.Errorf("empty status letter")
}
switch letter[0] {
case 'A':
return "added", nil
case 'D':
return "deleted", nil
case 'M':
return "modified", nil
case 'R':
// This is of the form "R<score>" but we are choosing to ignore the score
return "renamed", nil
case 'C':
// This is of the form "C<score>" but we are choosing to ignore the score
return "copied", nil
case 'T':
return "typechanged", nil
case 'U':
return "unmerged", nil
case 'X':
return "unknown", nil
default:
return "", fmt.Errorf("unknown status letter: '%s'", letter)
}
}

@ -0,0 +1,424 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitdiff
import (
"strings"
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/git"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGitDiffTree(t *testing.T) {
test := []struct {
Name string
RepoPath string
BaseSha string
HeadSha string
useMergeBase bool
Expected *DiffTree
}{
{
Name: "happy path",
RepoPath: "./testdata/academic-module",
BaseSha: "4d3d22609b895d43c2ad21096dc44a875ead8248",
HeadSha: "559c156f8e0178b71cb44355428f24001b08fc68",
Expected: &DiffTree{
Files: []*DiffTreeRecord{
{
Status: "modified",
HeadPath: "Http/Controllers/CurriculumController.php",
BasePath: "Http/Controllers/CurriculumController.php",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeBlob,
HeadBlobID: "cb993acce67a5d43d40f0fd321f6be903ed945c2",
BaseBlobID: "0b64a81851e374dcf25348a8f2c337e8993715a5",
},
{
Status: "modified",
HeadPath: "Http/Controllers/PeopleController.php",
BasePath: "Http/Controllers/PeopleController.php",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeBlob,
HeadBlobID: "b805a865244ca2615203f7f878fdefe69abf3054",
BaseBlobID: "942504b968c56022543915e08e19781d63f03ab6",
},
{
Status: "modified",
HeadPath: "Http/Controllers/ProgramController.php",
BasePath: "Http/Controllers/ProgramController.php",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeBlob,
HeadBlobID: "b21611d5cf3e0d2af82791a8d70a2357f8517c48",
BaseBlobID: "cc0c2f4f511b04f94ef6b2e08de9db8b74092d18",
},
{
Status: "modified",
HeadPath: "Http/Controllers/ProgramDirectorController.php",
BasePath: "Http/Controllers/ProgramDirectorController.php",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeBlob,
HeadBlobID: "1f41dc7d61ef1f85ad3f26814d077ea48376808a",
BaseBlobID: "70eefec0be7e0aff7877ed6290acfd0ca7417c79",
},
},
},
},
{
Name: "first commit (no parent)",
RepoPath: "./testdata/academic-module",
HeadSha: "07901f79ee86272fa8935f2fe546273adaf02c89",
Expected: &DiffTree{
Files: []*DiffTreeRecord{
{
Status: "added",
HeadPath: "README.md",
BasePath: "README.md",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeNoEntry,
HeadBlobID: "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
BaseBlobID: "0000000000000000000000000000000000000000",
},
},
},
},
{
Name: "first commit (no parent), merge base = true",
RepoPath: "./testdata/academic-module",
HeadSha: "07901f79ee86272fa8935f2fe546273adaf02c89",
useMergeBase: true,
Expected: &DiffTree{
Files: []*DiffTreeRecord{
{
Status: "added",
HeadPath: "README.md",
BasePath: "README.md",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeNoEntry,
HeadBlobID: "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
BaseBlobID: "0000000000000000000000000000000000000000",
},
},
},
},
{
Name: "base and head same",
RepoPath: "./testdata/academic-module",
BaseSha: "07901f79ee86272fa8935f2fe546273adaf02c89",
HeadSha: "07901f79ee86272fa8935f2fe546273adaf02c89",
Expected: &DiffTree{
Files: []*DiffTreeRecord{},
},
},
{
Name: "file renamed",
RepoPath: "./testdata/academic-module",
HeadSha: "6b8722c210ee91853f77b7bb8b4b3ce706088a03",
Expected: &DiffTree{
Files: []*DiffTreeRecord{
{
Status: "renamed",
HeadPath: "Database/Seeders/AcademicDatabaseSeeder.php",
BasePath: "Database/Seeders/AdminDatabaseSeeder.php",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeBlob,
HeadBlobID: "97248f79a90aaf81fe7fd74b33c1cb182dd41783",
BaseBlobID: "c8a055cfb45cd39747292983ad1797ceab40f5b1",
},
},
},
},
{
Name: "useMergeBase false",
RepoPath: "./testdata/academic-module",
BaseSha: "819dc756646ffd0730a163b5a8da65b84a6d504e",
HeadSha: "528846b39d8f7e68e8081d586ea3e94be23afa7f", // this commit can be found on the update-readme branch
useMergeBase: false,
Expected: &DiffTree{
Files: []*DiffTreeRecord{
{
Status: "modified",
HeadPath: "README.md",
BasePath: "README.md",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeBlob,
HeadBlobID: "ca07ea08ae0fa243d6e0a4129843c5a25a02f499",
BaseBlobID: "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
},
{
Status: "modified",
HeadPath: "webpack.mix.js",
BasePath: "webpack.mix.js",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeBlob,
HeadBlobID: "26825d9cb822e237b600153a26dd1e0e68457196",
BaseBlobID: "344e0ca8aa791cc4164fb0ea645f334fd40d00f0",
},
},
},
},
{
Name: "useMergeBase true",
RepoPath: "./testdata/academic-module",
BaseSha: "819dc756646ffd0730a163b5a8da65b84a6d504e",
HeadSha: "528846b39d8f7e68e8081d586ea3e94be23afa7f", // this commit can be found on the update-readme branch
useMergeBase: true,
Expected: &DiffTree{
Files: []*DiffTreeRecord{
{
Status: "modified",
HeadPath: "README.md",
BasePath: "README.md",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeBlob,
HeadBlobID: "ca07ea08ae0fa243d6e0a4129843c5a25a02f499",
BaseBlobID: "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
},
},
},
},
{
Name: "no base set",
RepoPath: "./testdata/academic-module",
HeadSha: "528846b39d8f7e68e8081d586ea3e94be23afa7f", // this commit can be found on the update-readme branch
useMergeBase: false,
Expected: &DiffTree{
Files: []*DiffTreeRecord{
{
Status: "modified",
HeadPath: "README.md",
BasePath: "README.md",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeBlob,
HeadBlobID: "ca07ea08ae0fa243d6e0a4129843c5a25a02f499",
BaseBlobID: "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
},
},
},
},
}
for _, tt := range test {
t.Run(tt.Name, func(t *testing.T) {
gitRepo, err := git.OpenRepository(git.DefaultContext, tt.RepoPath)
assert.NoError(t, err)
defer gitRepo.Close()
diffPaths, err := GetDiffTree(db.DefaultContext, gitRepo, tt.useMergeBase, tt.BaseSha, tt.HeadSha)
require.NoError(t, err)
assert.Equal(t, tt.Expected, diffPaths)
})
}
}
func TestGitDiffTreeErrors(t *testing.T) {
test := []struct {
Name string
RepoPath string
BaseSha string
HeadSha string
}{
{
Name: "head doesn't exist",
RepoPath: "./testdata/academic-module",
BaseSha: "4d3d22609b895d43c2ad21096dc44a875ead8248",
HeadSha: "asdfasdfasdf",
},
{
Name: "base doesn't exist",
RepoPath: "./testdata/academic-module",
BaseSha: "asdfasdfasdf",
HeadSha: "07901f79ee86272fa8935f2fe546273adaf02c89",
},
{
Name: "head not set",
RepoPath: "./testdata/academic-module",
BaseSha: "07901f79ee86272fa8935f2fe546273adaf02c89",
},
}
for _, tt := range test {
t.Run(tt.Name, func(t *testing.T) {
gitRepo, err := git.OpenRepository(git.DefaultContext, tt.RepoPath)
assert.NoError(t, err)
defer gitRepo.Close()
diffPaths, err := GetDiffTree(db.DefaultContext, gitRepo, true, tt.BaseSha, tt.HeadSha)
assert.Error(t, err)
assert.Nil(t, diffPaths)
})
}
}
func TestParseGitDiffTree(t *testing.T) {
test := []struct {
Name string
GitOutput string
Expected []*DiffTreeRecord
}{
{
Name: "file change",
GitOutput: ":100644 100644 64e43d23bcd08db12563a0a4d84309cadb437e1a 5dbc7792b5bb228647cfcc8dfe65fc649119dedc M\tResources/views/curriculum/edit.blade.php",
Expected: []*DiffTreeRecord{
{
Status: "modified",
HeadPath: "Resources/views/curriculum/edit.blade.php",
BasePath: "Resources/views/curriculum/edit.blade.php",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeBlob,
HeadBlobID: "5dbc7792b5bb228647cfcc8dfe65fc649119dedc",
BaseBlobID: "64e43d23bcd08db12563a0a4d84309cadb437e1a",
},
},
},
{
Name: "file added",
GitOutput: ":000000 100644 0000000000000000000000000000000000000000 0063162fb403db15ceb0517b34ab782e4e58b619 A\tResources/views/class/index.blade.php",
Expected: []*DiffTreeRecord{
{
Status: "added",
HeadPath: "Resources/views/class/index.blade.php",
BasePath: "Resources/views/class/index.blade.php",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeNoEntry,
HeadBlobID: "0063162fb403db15ceb0517b34ab782e4e58b619",
BaseBlobID: "0000000000000000000000000000000000000000",
},
},
},
{
Name: "file deleted",
GitOutput: ":100644 000000 bac4286303c8c0017ea2f0a48c561ddcc0330a14 0000000000000000000000000000000000000000 D\tResources/views/classes/index.blade.php",
Expected: []*DiffTreeRecord{
{
Status: "deleted",
HeadPath: "Resources/views/classes/index.blade.php",
BasePath: "Resources/views/classes/index.blade.php",
HeadMode: git.EntryModeNoEntry,
BaseMode: git.EntryModeBlob,
HeadBlobID: "0000000000000000000000000000000000000000",
BaseBlobID: "bac4286303c8c0017ea2f0a48c561ddcc0330a14",
},
},
},
{
Name: "file renamed",
GitOutput: ":100644 100644 c8a055cfb45cd39747292983ad1797ceab40f5b1 97248f79a90aaf81fe7fd74b33c1cb182dd41783 R087\tDatabase/Seeders/AdminDatabaseSeeder.php\tDatabase/Seeders/AcademicDatabaseSeeder.php",
Expected: []*DiffTreeRecord{
{
Status: "renamed",
HeadPath: "Database/Seeders/AcademicDatabaseSeeder.php",
BasePath: "Database/Seeders/AdminDatabaseSeeder.php",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeBlob,
HeadBlobID: "97248f79a90aaf81fe7fd74b33c1cb182dd41783",
BaseBlobID: "c8a055cfb45cd39747292983ad1797ceab40f5b1",
},
},
},
{
Name: "no changes",
GitOutput: ``,
Expected: []*DiffTreeRecord{},
},
{
Name: "multiple changes",
GitOutput: ":000000 100644 0000000000000000000000000000000000000000 db736b44533a840981f1f17b7029d0f612b69550 A\tHttp/Controllers/ClassController.php\n" +
":100644 000000 9a4d2344d4d0145db7c91b3f3e123c74367d4ef4 0000000000000000000000000000000000000000 D\tHttp/Controllers/ClassesController.php\n" +
":100644 100644 f060d6aede65d423f49e7dc248dfa0d8835ef920 b82c8e39a3602dedadb44669956d6eb5b6a7cc86 M\tHttp/Controllers/ProgramDirectorController.php\n",
Expected: []*DiffTreeRecord{
{
Status: "added",
HeadPath: "Http/Controllers/ClassController.php",
BasePath: "Http/Controllers/ClassController.php",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeNoEntry,
HeadBlobID: "db736b44533a840981f1f17b7029d0f612b69550",
BaseBlobID: "0000000000000000000000000000000000000000",
},
{
Status: "deleted",
HeadPath: "Http/Controllers/ClassesController.php",
BasePath: "Http/Controllers/ClassesController.php",
HeadMode: git.EntryModeNoEntry,
BaseMode: git.EntryModeBlob,
HeadBlobID: "0000000000000000000000000000000000000000",
BaseBlobID: "9a4d2344d4d0145db7c91b3f3e123c74367d4ef4",
},
{
Status: "modified",
HeadPath: "Http/Controllers/ProgramDirectorController.php",
BasePath: "Http/Controllers/ProgramDirectorController.php",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeBlob,
HeadBlobID: "b82c8e39a3602dedadb44669956d6eb5b6a7cc86",
BaseBlobID: "f060d6aede65d423f49e7dc248dfa0d8835ef920",
},
},
},
{
Name: "spaces in file path",
GitOutput: ":000000 100644 0000000000000000000000000000000000000000 db736b44533a840981f1f17b7029d0f612b69550 A\tHttp /Controllers/Class Controller.php\n" +
":100644 000000 9a4d2344d4d0145db7c91b3f3e123c74367d4ef4 0000000000000000000000000000000000000000 D\tHttp/Cont rollers/Classes Controller.php\n" +
":100644 100644 f060d6aede65d423f49e7dc248dfa0d8835ef920 b82c8e39a3602dedadb44669956d6eb5b6a7cc86 R\tHttp/Controllers/Program Director Controller.php\tHttp/Cont rollers/ProgramDirectorController.php\n",
Expected: []*DiffTreeRecord{
{
Status: "added",
HeadPath: "Http /Controllers/Class Controller.php",
BasePath: "Http /Controllers/Class Controller.php",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeNoEntry,
HeadBlobID: "db736b44533a840981f1f17b7029d0f612b69550",
BaseBlobID: "0000000000000000000000000000000000000000",
},
{
Status: "deleted",
HeadPath: "Http/Cont rollers/Classes Controller.php",
BasePath: "Http/Cont rollers/Classes Controller.php",
HeadMode: git.EntryModeNoEntry,
BaseMode: git.EntryModeBlob,
HeadBlobID: "0000000000000000000000000000000000000000",
BaseBlobID: "9a4d2344d4d0145db7c91b3f3e123c74367d4ef4",
},
{
Status: "renamed",
HeadPath: "Http/Cont rollers/ProgramDirectorController.php",
BasePath: "Http/Controllers/Program Director Controller.php",
HeadMode: git.EntryModeBlob,
BaseMode: git.EntryModeBlob,
HeadBlobID: "b82c8e39a3602dedadb44669956d6eb5b6a7cc86",
BaseBlobID: "f060d6aede65d423f49e7dc248dfa0d8835ef920",
},
},
},
{
Name: "file type changed",
GitOutput: ":100644 120000 344e0ca8aa791cc4164fb0ea645f334fd40d00f0 a7c2973de00bfdc6ca51d315f401b5199fe01dc3 T\twebpack.mix.js",
Expected: []*DiffTreeRecord{
{
Status: "typechanged",
HeadPath: "webpack.mix.js",
BasePath: "webpack.mix.js",
HeadMode: git.EntryModeSymlink,
BaseMode: git.EntryModeBlob,
HeadBlobID: "a7c2973de00bfdc6ca51d315f401b5199fe01dc3",
BaseBlobID: "344e0ca8aa791cc4164fb0ea645f334fd40d00f0",
},
},
},
}
for _, tt := range test {
t.Run(tt.Name, func(t *testing.T) {
entries, err := parseGitDiffTree(strings.NewReader(tt.GitOutput))
assert.NoError(t, err)
assert.Equal(t, tt.Expected, entries)
})
}
}

@ -1,7 +1,7 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
bare = true
logallrefupdates = true
ignorecase = true
precomposeunicode = true

@ -1 +1,2 @@
0000000000000000000000000000000000000000 bd7063cc7c04689c4d082183d32a604ed27a24f9 Lunny Xiao <xiaolunwen@gmail.com> 1574829684 +0800 clone: from https://try.gitea.io/shemgp-aiias/academic-module
0000000000000000000000000000000000000000 819dc756646ffd0730a163b5a8da65b84a6d504e Alexander McRae <alex@allspice.io> 1737998543 -0800 push

@ -1 +1,2 @@
0000000000000000000000000000000000000000 bd7063cc7c04689c4d082183d32a604ed27a24f9 Lunny Xiao <xiaolunwen@gmail.com> 1574829684 +0800 clone: from https://try.gitea.io/shemgp-aiias/academic-module
bd7063cc7c04689c4d082183d32a604ed27a24f9 819dc756646ffd0730a163b5a8da65b84a6d504e Alexander McRae <alex@allspice.io> 1737998543 -0800 push

@ -0,0 +1 @@
0000000000000000000000000000000000000000 528846b39d8f7e68e8081d586ea3e94be23afa7f Alexander McRae <alex@allspice.io> 1737998161 -0800 push

@ -0,0 +1,2 @@
x…ŹŃJ!†»ö)ćNĄłÝDĐE°ôWgĎqYusÜC˝{*§ş AýüćgfZÓ÷w76E*ü;<AĆ·Ýg|5Ů\p*ćň‘ýŇ€ů„C0ŃĎHĄi¬ęŠ°Śű´z;šr\)]÷Ö —Ş˝\kÄwÍBâxt>Gn<EFBFBD>ëW¤´g‹¤ Ő@ŇK˝m›Z<EFBFBD>€·‡5<EFBFBD>·ŤHu)ŞŞ€ršÔ“ČRĎŞÇoXc­'­áy†-'·ŰâS<€qĘá‚™*đńÄü ˘ ěăř#
)᣷Ó~®ró“}eůz”

@ -1 +1 @@
bd7063cc7c04689c4d082183d32a604ed27a24f9
819dc756646ffd0730a163b5a8da65b84a6d504e

@ -0,0 +1 @@
528846b39d8f7e68e8081d586ea3e94be23afa7f
Loading…
Cancel
Save