I misspelled an identifier in one spot and then just autocompleted to victory everywhere else. This commit fixes the typo!
1153 lines
36 KiB
Go
1153 lines
36 KiB
Go
package git
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kylelemons/godebug/pretty"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/credentialspb"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/process"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/sourcestest"
|
|
)
|
|
|
|
func TestClone_Timeout(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("an unset timeout should not cause a timeout", func(t *testing.T) {
|
|
_, _, err := CloneRepo(
|
|
ctx,
|
|
nil,
|
|
"https://github.com/dustin-decker/secretsandstuff.git",
|
|
"",
|
|
false)
|
|
|
|
// There shouldn't be an error - but if there is because of some other problem, we don't want this test to fail
|
|
if err != nil {
|
|
assert.NotContains(t, err.Error(), "timed out")
|
|
}
|
|
})
|
|
|
|
t.Run("a clone that times out should time out", func(t *testing.T) {
|
|
feature.GitCloneTimeoutDuration.Store(int64(1 * time.Nanosecond))
|
|
t.Cleanup(func() { feature.GitCloneTimeoutDuration.Store(0) })
|
|
|
|
_, _, err := CloneRepo(
|
|
ctx,
|
|
nil,
|
|
"https://github.com/dustin-decker/secretsandstuff.git",
|
|
"",
|
|
false)
|
|
|
|
if assert.Error(t, err) {
|
|
assert.Contains(t, err.Error(), "timed out")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSource_Scan(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
secret, err := common.GetTestSecret(ctx)
|
|
if err != nil {
|
|
t.Fatal(fmt.Errorf("failed to access secret: %v", err))
|
|
}
|
|
basicUser := secret.MustGetField("GITLAB_USER")
|
|
basicPass := secret.MustGetField("GITLAB_PASS")
|
|
|
|
type init struct {
|
|
name string
|
|
verify bool
|
|
connection *sourcespb.Git
|
|
concurrency int
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
init init
|
|
wantChunk *sources.Chunk
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "local repo",
|
|
init: init{
|
|
name: "this repo",
|
|
connection: &sourcespb.Git{
|
|
Directories: []string{"../../../"},
|
|
Credential: &sourcespb.Git_Unauthenticated{
|
|
Unauthenticated: &credentialspb.Unauthenticated{},
|
|
},
|
|
},
|
|
concurrency: 4,
|
|
},
|
|
wantChunk: &sources.Chunk{
|
|
SourceType: sourcespb.SourceType_SOURCE_TYPE_GIT,
|
|
SourceName: "this repo",
|
|
Verify: false,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "remote repo, unauthenticated",
|
|
init: init{
|
|
name: "test source",
|
|
connection: &sourcespb.Git{
|
|
Repositories: []string{"https://github.com/dustin-decker/secretsandstuff.git"},
|
|
Credential: &sourcespb.Git_Unauthenticated{
|
|
Unauthenticated: &credentialspb.Unauthenticated{},
|
|
},
|
|
},
|
|
concurrency: 4,
|
|
},
|
|
wantChunk: &sources.Chunk{
|
|
SourceType: sourcespb.SourceType_SOURCE_TYPE_GIT,
|
|
SourceName: "test source",
|
|
Verify: false,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "remote repo, unauthenticated, concurrency 0",
|
|
init: init{
|
|
name: "test source",
|
|
connection: &sourcespb.Git{
|
|
Repositories: []string{"https://github.com/dustin-decker/secretsandstuff.git"},
|
|
Credential: &sourcespb.Git_Unauthenticated{
|
|
Unauthenticated: &credentialspb.Unauthenticated{},
|
|
},
|
|
},
|
|
concurrency: 0,
|
|
},
|
|
wantChunk: &sources.Chunk{
|
|
SourceType: sourcespb.SourceType_SOURCE_TYPE_GIT,
|
|
SourceName: "test source",
|
|
Verify: false,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "remote repo, basic auth",
|
|
init: init{
|
|
name: "test source",
|
|
connection: &sourcespb.Git{
|
|
Repositories: []string{"https://github.com/dustin-decker/secretsandstuff.git"},
|
|
Credential: &sourcespb.Git_BasicAuth{
|
|
BasicAuth: &credentialspb.BasicAuth{
|
|
Username: basicUser,
|
|
Password: basicPass,
|
|
},
|
|
},
|
|
},
|
|
concurrency: 4,
|
|
},
|
|
wantChunk: &sources.Chunk{
|
|
SourceType: sourcespb.SourceType_SOURCE_TYPE_GIT,
|
|
SourceName: "test source",
|
|
Verify: false,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
s := Source{}
|
|
|
|
conn, err := anypb.New(tt.init.connection)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = s.Init(ctx, tt.init.name, 0, 0, tt.init.verify, conn, tt.init.concurrency)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Source.Init() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
chunksCh := make(chan *sources.Chunk, 1)
|
|
go func() {
|
|
assert.NoError(t, s.Chunks(ctx, chunksCh))
|
|
}()
|
|
gotChunk := <-chunksCh
|
|
gotChunk.Data = nil
|
|
// Commits don't come in a deterministic order, so remove metadata comparison
|
|
gotChunk.SourceMetadata = nil
|
|
if diff := pretty.Compare(gotChunk, tt.wantChunk); diff != "" {
|
|
t.Errorf("Source.Chunks() %s diff: (-got +want)\n%s", tt.name, diff)
|
|
t.Errorf("Data: %s", string(gotChunk.Data))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// We ran into an issue where upgrading a dependency caused the git patch chunking to break
|
|
// So this test exists to make sure that when something changes, we know about it.
|
|
func TestSource_Chunks_Integration(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
type init struct {
|
|
name string
|
|
verify bool
|
|
connection *sourcespb.Git
|
|
}
|
|
|
|
type byteCompare struct {
|
|
B []byte
|
|
Found bool
|
|
Multi bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
init init
|
|
// verified
|
|
repoURL string
|
|
expectedChunkData map[string]*byteCompare
|
|
scanOptions ScanOptions
|
|
}{
|
|
{
|
|
name: "remote repo, unauthenticated",
|
|
repoURL: "https://github.com/dustin-decker/secretsandstuff.git",
|
|
expectedChunkData: map[string]*byteCompare{
|
|
"70001020fab32b1fcf2f1f0e5c66424eae649826-": {B: []byte("Dustin Decker <[email protected]>\nGitHub <[email protected]>\nUpdate aws\n")},
|
|
"70001020fab32b1fcf2f1f0e5c66424eae649826-aws": {B: []byte("[default]\naws_access_key_id = AKIAXYZDQCEN4B6JSJQI\naws_secret_access_key = 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\noutput = json\nregion = us-east-2\n")},
|
|
"a6f8aa55736d4a85be31a0048a4607396898647a-": {B: []byte("Dustin Decker <[email protected]>\nGitHub <[email protected]>\nUpdate bump\n")},
|
|
"a6f8aa55736d4a85be31a0048a4607396898647a-bump": {B: []byte("\n\nf\n")},
|
|
"73ab4713057944753f1bdeb80e757380e64c6b5b-": {B: []byte("Dustin <[email protected]>\nDustin <[email protected]>\nbump\n")},
|
|
"73ab4713057944753f1bdeb80e757380e64c6b5b-bump": {B: []byte(" s \n\n")},
|
|
"2f251b8c1e72135a375b659951097ec7749d4af9-": {B: []byte("Dustin <[email protected]>\nDustin <[email protected]>\nbump\n")},
|
|
"2f251b8c1e72135a375b659951097ec7749d4af9-bump": {B: []byte(" \n\n")},
|
|
"e6c8bbabd8796ea3cd85bfc2e55b27e0a491747f-": {B: []byte("Dustin Decker <[email protected]>\nGitHub <[email protected]>\nUpdate bump\n")},
|
|
"e6c8bbabd8796ea3cd85bfc2e55b27e0a491747f-bump": {B: []byte("\noops \n")},
|
|
"735b52b0eb40610002bb1088e902bd61824eb305-": {B: []byte("Dustin Decker <[email protected]>\nGitHub <[email protected]>\nUpdate bump\n")},
|
|
"735b52b0eb40610002bb1088e902bd61824eb305-bump": {B: []byte("\noops\n")},
|
|
"ce62d79908803153ef6e145e042d3e80488ef747-": {B: []byte("Dustin Decker <[email protected]>\nGitHub <[email protected]>\nCreate bump\n")},
|
|
"ce62d79908803153ef6e145e042d3e80488ef747-bump": {B: []byte("\n")},
|
|
// Normally we might expect to see this commit, and we may in the future.
|
|
// But at the moment we're ignoring any commit unless it contains at least one non-space character.
|
|
"27fbead3bf883cdb7de9d7825ed401f28f9398f1-": {B: []byte("Dustin <[email protected]>\nDustin <[email protected]>\noops\n")},
|
|
"27fbead3bf883cdb7de9d7825ed401f28f9398f1-slack": {B: []byte("\n\n\nyup, just did that\n\ngithub_lol: \"ffc7e0f9400fb6300167009e42d2f842cd7956e2\"\n\noh, goodness. there's another one!\n")},
|
|
"8afb0ecd4998b1179e428db5ebbcdc8221214432-": {B: []byte("Dustin <[email protected]>\nDustin <[email protected]>\nadd slack token\n")},
|
|
"8afb0ecd4998b1179e428db5ebbcdc8221214432-slack": {B: []byte("oops might drop a slack token here\n\ngithub_secret=\"369963c1434c377428ca8531fbc46c0c43d037a0\"\n\nyup, just did that\n"), Multi: true},
|
|
"8fe6f04ef1839e3fc54b5147e3d0e0b7ab971bd5-": {B: []byte("Dustin <[email protected]>\nDustin <[email protected]>\noops, accidently commited AWS token...\n")}, //nolint:misspell
|
|
"8fe6f04ef1839e3fc54b5147e3d0e0b7ab971bd5-aws": {B: []byte("blah blaj\n\nthis is the secret: AKIA2E0A8F3B244C9986\n\nokay thank you bye\n"), Multi: true},
|
|
"84e9c75e388ae3e866e121087ea2dd45a71068f2-": {B: []byte("Dylan Ayrey <[email protected]>\nGitHub <[email protected]>\nUpdate aws\n")},
|
|
"84e9c75e388ae3e866e121087ea2dd45a71068f2-aws": {B: []byte("\n\nthis is the secret: [Default]\nAccess key Id: AKIAILE3JG6KMS3HZGCA\nSecret Access Key: 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\n\nokay thank you bye\n"), Multi: false},
|
|
},
|
|
},
|
|
{
|
|
name: "remote repo, limited",
|
|
repoURL: "https://github.com/dustin-decker/secretsandstuff.git",
|
|
expectedChunkData: map[string]*byteCompare{
|
|
"70001020fab32b1fcf2f1f0e5c66424eae649826-": {B: []byte("Dustin Decker <[email protected]>\nGitHub <[email protected]>\nUpdate aws\n")},
|
|
"70001020fab32b1fcf2f1f0e5c66424eae649826-aws": {B: []byte("[default]\naws_access_key_id = AKIAXYZDQCEN4B6JSJQI\naws_secret_access_key = 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\noutput = json\nregion = us-east-2\n")},
|
|
},
|
|
scanOptions: ScanOptions{
|
|
HeadHash: "70001020fab32b1fcf2f1f0e5c66424eae649826",
|
|
BaseHash: "a6f8aa55736d4a85be31a0048a4607396898647a",
|
|
},
|
|
},
|
|
{
|
|
name: "remote repo, main ahead of branch",
|
|
repoURL: "https://github.com/bill-rich/bad-secrets.git",
|
|
expectedChunkData: map[string]*byteCompare{
|
|
"547865c6cc0da46622306902b1b66f7e25dd0412-": {B: []byte("bill-rich <[email protected]>\nbill-rich <[email protected]>\nAdd some_branch_file\n")},
|
|
"547865c6cc0da46622306902b1b66f7e25dd0412-some_branch_file": {B: []byte("[default]\naws_access_key=AKIAYVP4CIPPH5TNP3SW\naws_secret_access_key=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\nregion=us-east-2\noutput=json\n\n#addibng a comment\n")},
|
|
},
|
|
scanOptions: ScanOptions{
|
|
HeadHash: "some_branch",
|
|
BaseHash: "master",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
s := Source{}
|
|
|
|
beforeProcesses := process.GetGitProcessList()
|
|
|
|
conn, err := anypb.New(tt.init.connection)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = s.Init(ctx, tt.init.name, 0, 0, tt.init.verify, conn, 4)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
chunksCh := make(chan *sources.Chunk, 1)
|
|
go func() {
|
|
defer close(chunksCh)
|
|
repoPath, repo, err := CloneRepoUsingUnauthenticated(ctx, tt.repoURL, "")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = s.git.ScanRepo(ctx, repo, repoPath, &tt.scanOptions, sources.ChanReporter{Ch: chunksCh})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
for chunk := range chunksCh {
|
|
key := ""
|
|
switch meta := chunk.SourceMetadata.GetData().(type) {
|
|
case *source_metadatapb.MetaData_Git:
|
|
key = strings.TrimRight(meta.Git.Commit+"-"+meta.Git.File, "\n")
|
|
}
|
|
|
|
if expectedData, exists := tt.expectedChunkData[key]; !exists {
|
|
t.Errorf("A chunk exists that was not expected with key %q", key)
|
|
} else {
|
|
if bytes.Equal(chunk.Data, expectedData.B) {
|
|
(*tt.expectedChunkData[key]).Found = true
|
|
} else if !expectedData.Multi {
|
|
t.Errorf("Got %q: %q, which was not expected", key, string(chunk.Data))
|
|
}
|
|
}
|
|
}
|
|
|
|
for key, expected := range tt.expectedChunkData {
|
|
if !expected.Found {
|
|
t.Errorf("Expected data with key %q not found", key)
|
|
}
|
|
|
|
}
|
|
|
|
afterProcesses := process.GetGitProcessList()
|
|
zombies := process.DetectGitZombies(beforeProcesses, afterProcesses)
|
|
if len(zombies) > 0 {
|
|
t.Errorf("Git zombies detected: %v", zombies)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSource_Chunks_Edge_Cases(t *testing.T) {
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
secret, err := common.GetTestSecret(ctx)
|
|
if err != nil {
|
|
t.Fatal(fmt.Errorf("failed to access secret: %v", err))
|
|
}
|
|
basicUser := secret.MustGetField("GITLAB_USER")
|
|
basicPass := secret.MustGetField("GITLAB_PASS")
|
|
|
|
type init struct {
|
|
name string
|
|
verify bool
|
|
connection *sourcespb.Git
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
init init
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "empty repo",
|
|
init: init{
|
|
name: "test source",
|
|
connection: &sourcespb.Git{
|
|
Repositories: []string{"https://github.com/git-fixtures/empty.git"},
|
|
Credential: &sourcespb.Git_Unauthenticated{
|
|
Unauthenticated: &credentialspb.Unauthenticated{},
|
|
},
|
|
},
|
|
},
|
|
wantErr: "remote",
|
|
},
|
|
{
|
|
name: "no repo",
|
|
init: init{
|
|
name: "test source",
|
|
connection: &sourcespb.Git{
|
|
Repositories: []string{""},
|
|
Credential: &sourcespb.Git_Unauthenticated{
|
|
Unauthenticated: &credentialspb.Unauthenticated{},
|
|
},
|
|
},
|
|
},
|
|
wantErr: "remote",
|
|
},
|
|
{
|
|
name: "no repo, basic auth",
|
|
init: init{
|
|
name: "test source",
|
|
connection: &sourcespb.Git{
|
|
Repositories: []string{""},
|
|
Credential: &sourcespb.Git_BasicAuth{
|
|
BasicAuth: &credentialspb.BasicAuth{
|
|
Username: basicUser,
|
|
Password: basicPass,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: "remote",
|
|
},
|
|
{
|
|
name: "symlinks repo",
|
|
init: init{
|
|
name: "test source",
|
|
connection: &sourcespb.Git{
|
|
Repositories: []string{"https://github.com/git-fixtures/symlinks.git"},
|
|
Credential: &sourcespb.Git_Unauthenticated{
|
|
Unauthenticated: &credentialspb.Unauthenticated{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "submodule repo",
|
|
init: init{
|
|
name: "test source",
|
|
connection: &sourcespb.Git{
|
|
Repositories: []string{"https://github.com/git-fixtures/submodule.git"},
|
|
Credential: &sourcespb.Git_Unauthenticated{
|
|
Unauthenticated: &credentialspb.Unauthenticated{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
s := Source{}
|
|
|
|
conn, err := anypb.New(tt.init.connection)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = s.Init(ctx, tt.init.name, 0, 0, tt.init.verify, conn, 4)
|
|
if err != nil {
|
|
t.Errorf("Source.Init() error = %v", err)
|
|
return
|
|
}
|
|
chunksCh := make(chan *sources.Chunk, 1)
|
|
go func() {
|
|
for chunk := range chunksCh {
|
|
chunk.Data = nil
|
|
}
|
|
|
|
}()
|
|
if err := s.Chunks(ctx, chunksCh); err != nil && !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Errorf("Source.Chunks() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPrepareRepo(t *testing.T) {
|
|
tests := []struct {
|
|
uri string
|
|
path bool
|
|
remote bool
|
|
err error
|
|
}{
|
|
{
|
|
uri: "https://github.com/dustin-decker/secretsandstuff.git",
|
|
path: true,
|
|
remote: true,
|
|
err: nil,
|
|
},
|
|
{
|
|
uri: "http://github.com/dustin-decker/secretsandstuff.git",
|
|
path: true,
|
|
remote: true,
|
|
err: nil,
|
|
},
|
|
{
|
|
uri: "file:///path/to/file.json",
|
|
path: true,
|
|
remote: false,
|
|
err: nil,
|
|
// Note: If we set trustLocalGitConfig to false (below), we will get an error for this test
|
|
// b/c it's not a valid git repo and we will try to clone it.
|
|
},
|
|
{
|
|
uri: "no bueno",
|
|
path: false,
|
|
remote: false,
|
|
err: fmt.Errorf("unsupported Git URI: no bueno"),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
ctx := context.Background()
|
|
repo, b, err := PrepareRepo(ctx, tt.uri, "", true, false)
|
|
var repoLen bool
|
|
if len(repo) > 0 {
|
|
repoLen = true
|
|
} else {
|
|
repoLen = false
|
|
}
|
|
if repoLen != tt.path || b != tt.remote {
|
|
t.Errorf("PrepareRepo(%v) got: %v, %v, %v want: %v, %v, %v", tt.uri, repo, b, err, tt.path, tt.remote, tt.err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkPrepareRepo(b *testing.B) {
|
|
uri := "https://github.com/dustin-decker/secretsandstuff.git"
|
|
ctx := context.Background()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _, _ = PrepareRepo(ctx, uri, "", false, false)
|
|
}
|
|
}
|
|
|
|
func TestGitURLParse(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
url string
|
|
host string
|
|
user string
|
|
password string
|
|
port string
|
|
path string
|
|
scheme string
|
|
}{
|
|
{
|
|
"https://[email protected]/org/repo",
|
|
"github.com",
|
|
"user",
|
|
"",
|
|
"",
|
|
"/org/repo",
|
|
"https",
|
|
},
|
|
{
|
|
"https://user:[email protected]/org/repo",
|
|
"github.com",
|
|
"user",
|
|
"pass",
|
|
"",
|
|
"/org/repo",
|
|
"https",
|
|
},
|
|
{
|
|
"ssh://[email protected]/org/repo",
|
|
"github.com",
|
|
"user",
|
|
"",
|
|
"",
|
|
"/org/repo",
|
|
"ssh",
|
|
},
|
|
{
|
|
"[email protected]:org/repo",
|
|
"github.com",
|
|
"user",
|
|
"",
|
|
"",
|
|
"/org/repo",
|
|
"ssh",
|
|
},
|
|
} {
|
|
u, err := GitURLParse(tt.url)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assert.Equal(t, tt.host, u.Host)
|
|
assert.Equal(t, tt.user, u.User.Username())
|
|
password, _ := u.User.Password()
|
|
assert.Equal(t, tt.password, password)
|
|
assert.Equal(t, tt.port, u.Port())
|
|
assert.Equal(t, tt.path, u.Path)
|
|
assert.Equal(t, tt.scheme, u.Scheme)
|
|
}
|
|
}
|
|
|
|
func TestEnumerate(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
// Setup the connection to test enumeration.
|
|
units := []string{
|
|
"foo", "bar", "baz",
|
|
"/path/to/dir/", "/path/to/another/dir/",
|
|
}
|
|
conn, err := anypb.New(&sourcespb.Git{
|
|
Repositories: units[0:3],
|
|
Directories: units[3:],
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
// Initialize the source.
|
|
s := Source{}
|
|
err = s.Init(ctx, "test enumerate", 0, 0, true, conn, 1)
|
|
assert.NoError(t, err)
|
|
|
|
reporter := sourcestest.TestReporter{}
|
|
err = s.Enumerate(ctx, &reporter)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, len(units), len(reporter.Units))
|
|
assert.Equal(t, 0, len(reporter.UnitErrs))
|
|
for _, unit := range reporter.Units {
|
|
id, _ := unit.SourceUnitID()
|
|
assert.Contains(t, units, id)
|
|
}
|
|
for _, unit := range units[:3] {
|
|
assert.Contains(t, reporter.Units, SourceUnit{ID: unit, Kind: UnitRepo})
|
|
}
|
|
for _, unit := range units[3:] {
|
|
assert.Contains(t, reporter.Units, SourceUnit{ID: unit, Kind: UnitDir})
|
|
}
|
|
}
|
|
|
|
func TestChunkUnit(t *testing.T) {
|
|
t.Skip("flaky - INS-212")
|
|
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
// Initialize the source.
|
|
s := Source{}
|
|
conn, err := anypb.New(&sourcespb.Git{
|
|
Credential: &sourcespb.Git_Unauthenticated{},
|
|
})
|
|
assert.NoError(t, err)
|
|
err = s.Init(ctx, "test chunk", 0, 0, true, conn, 1)
|
|
assert.NoError(t, err)
|
|
|
|
reporter := sourcestest.TestReporter{}
|
|
|
|
// Happy path single repository.
|
|
err = s.ChunkUnit(ctx, SourceUnit{
|
|
ID: "https://github.com/dustin-decker/secretsandstuff.git",
|
|
Kind: UnitRepo,
|
|
}, &reporter)
|
|
assert.NoError(t, err)
|
|
|
|
// Error path - should return fatal error for missing directory.
|
|
err = s.ChunkUnit(ctx, SourceUnit{
|
|
ID: "/file/not/found",
|
|
Kind: UnitDir,
|
|
}, &reporter)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "directory does not exist")
|
|
|
|
assert.Equal(t, 22, len(reporter.Chunks))
|
|
assert.Equal(t, 0, len(reporter.ChunkErrs))
|
|
}
|
|
|
|
func setupTestRepo(t *testing.T, repoName string) string {
|
|
tempDir := t.TempDir()
|
|
repoPath := filepath.Join(tempDir, repoName)
|
|
|
|
assert.NoError(t, exec.Command("git", "init", repoPath).Run())
|
|
assert.NoError(t, exec.Command("git", "-C", repoPath, "config", "user.name", "Test User").Run())
|
|
assert.NoError(t, exec.Command("git", "-C", repoPath, "config", "user.email", "[email protected]").Run())
|
|
|
|
return repoPath
|
|
}
|
|
|
|
func addTestFileAndCommit(t *testing.T, repoPath, filename, content string) {
|
|
testFile := filepath.Join(repoPath, filename)
|
|
assert.NoError(t, os.WriteFile(testFile, []byte(content), 0644))
|
|
assert.NoError(t, exec.Command("git", "-C", repoPath, "add", filename).Run())
|
|
assert.NoError(t, exec.Command("git", "-C", repoPath, "commit", "-m", "Test commit").Run())
|
|
}
|
|
|
|
func addMaliciousGitConfig(t *testing.T, repoPath string) {
|
|
assert.NoError(t, exec.Command("git", "-C", repoPath, "config", "alias.test", "!touch malicious_alias.txt").Run())
|
|
}
|
|
|
|
func testPrepareRepoSanitization(t *testing.T, repoPath string, trustLocalGitConfig, isBare bool) {
|
|
ctx := context.Background()
|
|
fileURI := "file://" + repoPath
|
|
|
|
preparedPath, isRemote, err := PrepareRepo(ctx, fileURI, "", trustLocalGitConfig, isBare)
|
|
|
|
assert.NoError(t, err)
|
|
assert.False(t, isRemote, "Local file URI should not be considered remote")
|
|
|
|
if !trustLocalGitConfig {
|
|
assert.NotEqual(t, repoPath, preparedPath, "Sanitized repo path should be different from original")
|
|
|
|
_, err := os.Stat(preparedPath)
|
|
assert.NoError(t, err, "Cloned repository should exist")
|
|
|
|
if isBare {
|
|
_, err = os.Stat(filepath.Join(preparedPath, "refs"))
|
|
assert.NoError(t, err, "Bare repo should have refs directory")
|
|
_, err = os.Stat(filepath.Join(preparedPath, "HEAD"))
|
|
assert.NoError(t, err, "Bare repo should have HEAD file")
|
|
_, err = os.Stat(filepath.Join(preparedPath, ".git"))
|
|
assert.True(t, os.IsNotExist(err), "Bare repo should not have .git subdirectory")
|
|
} else {
|
|
_, err = exec.Command("git", "-C", preparedPath, "status").Output()
|
|
assert.NoError(t, err, "Cloned repo should be a valid git repository")
|
|
}
|
|
} else {
|
|
assert.Equal(t, repoPath, preparedPath, "Trusted repo should use original path")
|
|
output, err := exec.Command("git", "-C", preparedPath, "config", "--get", "alias.test").Output()
|
|
assert.NoError(t, err, "Malicious git config should be present")
|
|
assert.Equal(t, "!touch malicious_alias.txt", strings.TrimSpace(string(output)), "Malicious git config should be present")
|
|
}
|
|
}
|
|
|
|
func TestGitConfigSanitization(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
repoPath := setupTestRepo(t, "test-repo")
|
|
addMaliciousGitConfig(t, repoPath)
|
|
addTestFileAndCommit(t, repoPath, "test.txt", "test content\nsecret: AKIA1234567890123456")
|
|
|
|
tests := []struct {
|
|
name string
|
|
trustLocalGitConfig bool
|
|
}{
|
|
{"sanitization enabled - should clone", false},
|
|
{"sanitization disabled - direct access", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
testPrepareRepoSanitization(t, repoPath, tt.trustLocalGitConfig, false)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitConfigSanitizationWithBareRepo(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
repoPath := setupTestRepo(t, "working-repo-original")
|
|
addTestFileAndCommit(t, repoPath, "test.txt", "test content\nsecret: AKIA1234567890123456")
|
|
|
|
tempDir := t.TempDir()
|
|
bareRepoPath := filepath.Join(tempDir, "working-repo")
|
|
assert.NoError(t, exec.Command("git", "clone", repoPath, bareRepoPath, "--bare").Run())
|
|
addMaliciousGitConfig(t, bareRepoPath)
|
|
|
|
tests := []struct {
|
|
name string
|
|
trustLocalGitConfig bool
|
|
isBare bool
|
|
}{
|
|
{"sanitization enabled - should clone bare repo", false, true},
|
|
{"sanitization disabled - direct access of bare repo", true, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
testPrepareRepoSanitization(t, bareRepoPath, tt.trustLocalGitConfig, tt.isBare)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitConfigSecurityIsolation(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
maliciousRepoPath := setupTestRepo(t, "malicious-repo")
|
|
addTestFileAndCommit(t, maliciousRepoPath, "test.txt", "test content")
|
|
addMaliciousGitConfig(t, maliciousRepoPath)
|
|
|
|
tests := []struct {
|
|
name string
|
|
trustLocalGitConfig bool
|
|
}{
|
|
{"sanitized repo should be isolated from malicious config", false},
|
|
{"trusted repo should use original path with all configs", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
fileURI := "file://" + maliciousRepoPath
|
|
preparedPath, isRemote, err := PrepareRepo(ctx, fileURI, "", tt.trustLocalGitConfig, false)
|
|
|
|
assert.NoError(t, err)
|
|
assert.False(t, isRemote)
|
|
|
|
maliciousFilePath := filepath.Join(preparedPath, "malicious_alias.txt")
|
|
|
|
if tt.trustLocalGitConfig {
|
|
assert.Equal(t, maliciousRepoPath, preparedPath, "Trusted repo should use original path")
|
|
|
|
output, err := exec.Command("git", "-C", preparedPath, "config", "--get", "alias.test").Output()
|
|
assert.NoError(t, err, "Config alias.test should exist in trusted (original) repo")
|
|
assert.Equal(t, "!touch malicious_alias.txt", strings.TrimSpace(string(output)),
|
|
"Config alias.test should have original dangerous value")
|
|
|
|
err = exec.Command("git", "-C", preparedPath, "test").Run()
|
|
assert.NoError(t, err)
|
|
_, err = os.Stat(maliciousFilePath)
|
|
assert.False(t, os.IsNotExist(err), "Malicious file malicious_alias.txt should exist in trusted environment")
|
|
|
|
} else {
|
|
assert.NotEqual(t, maliciousRepoPath, preparedPath, "Sanitized repo should be different from original")
|
|
|
|
_, err = os.Stat(preparedPath)
|
|
assert.NoError(t, err, "Sanitized repository should exist")
|
|
|
|
_, err = exec.Command("git", "-C", preparedPath, "status").Output()
|
|
assert.NoError(t, err, "Sanitized repo should be a valid git repository")
|
|
|
|
err = exec.Command("git", "-C", preparedPath, "test").Run()
|
|
assert.Error(t, err)
|
|
|
|
_, err = os.Stat(maliciousFilePath)
|
|
assert.True(t, os.IsNotExist(err), "Malicious file malicious_alias.txt should not exist in sanitized environment")
|
|
}
|
|
// if the file exists, remove it
|
|
if _, err := os.Stat(maliciousFilePath); err == nil {
|
|
assert.NoError(t, os.Remove(maliciousFilePath))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitConfigSanitizationWithStagedChanges(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
repoPath := setupTestRepo(t, "staged-changes-repo")
|
|
addMaliciousGitConfig(t, repoPath)
|
|
addTestFileAndCommit(t, repoPath, "test.txt", "test content\nsecret: AKIA1234567890123456")
|
|
|
|
testFile := filepath.Join(repoPath, "test.txt")
|
|
assert.NoError(t, os.WriteFile(testFile, []byte("modified content with secret: AKIA1234567890123456"), 0644))
|
|
assert.NoError(t, exec.Command("git", "-C", repoPath, "add", "test.txt").Run())
|
|
|
|
output, err := exec.Command("git", "-C", repoPath, "diff", "--cached").Output()
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, string(output), "modified content", "Staged changes should exist in original repo")
|
|
|
|
t.Run("staged changes preserved during sanitization", func(t *testing.T) {
|
|
fileURI := "file://" + repoPath
|
|
preparedPath, isRemote, err := PrepareRepo(ctx, fileURI, "", false, false) // Enable sanitization
|
|
|
|
assert.NoError(t, err)
|
|
assert.False(t, isRemote)
|
|
assert.NotEqual(t, repoPath, preparedPath, "Sanitized repo should be cloned")
|
|
|
|
stagedOutput, err := exec.Command("git", "-C", preparedPath, "diff", "--cached").Output()
|
|
if err == nil && len(stagedOutput) > 0 {
|
|
assert.Contains(t, string(stagedOutput), "modified content", "Staged changes should be preserved in cloned repo")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPrepareRepoErrorPaths(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
t.Run("clone failure should return error", func(t *testing.T) {
|
|
invalidFileURI := "file:///nonexistent/invalid/repo/path"
|
|
preparedPath, isRemote, err := PrepareRepo(ctx, invalidFileURI, "", false, false)
|
|
assert.Error(t, err)
|
|
assert.False(t, isRemote)
|
|
assert.Equal(t, "", preparedPath)
|
|
})
|
|
}
|
|
|
|
func TestNormalizeFileURI(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "absolute file URI unchanged",
|
|
input: "file:///absolute/path",
|
|
expected: "file:///absolute/path",
|
|
},
|
|
{
|
|
name: "relative file URI with current directory",
|
|
input: "file://.",
|
|
expected: "", // Will be set to current working directory
|
|
},
|
|
{
|
|
name: "relative file URI with subdirectory",
|
|
input: "file://./subdir",
|
|
expected: "", // Will be set to current working directory + subdir
|
|
},
|
|
{
|
|
name: "relative file URI with parent directory",
|
|
input: "file://..",
|
|
expected: "", // Will be set to parent of current working directory
|
|
},
|
|
{
|
|
name: "non-file URI unchanged",
|
|
input: "https://github.com/user/repo.git",
|
|
expected: "https://github.com/user/repo.git",
|
|
},
|
|
{
|
|
name: "file URI with host and path",
|
|
input: "file://hostname/path",
|
|
expected: "", // Will be set to absolute path of hostname/path
|
|
},
|
|
}
|
|
|
|
// Get current working directory for expected results
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("failed to get current working directory: %v", err)
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
inputURI, err := GitURLParse(tt.input)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse input URI: %v", err)
|
|
}
|
|
|
|
result, err := normalizeFileURI(inputURI)
|
|
assert.NoError(t, err)
|
|
|
|
var expected string
|
|
switch tt.name {
|
|
case "relative file URI with current directory":
|
|
expected = "file://" + cwd
|
|
case "relative file URI with subdirectory":
|
|
expected = "file://" + filepath.Join(cwd, "subdir")
|
|
case "relative file URI with parent directory":
|
|
expected = "file://" + filepath.Dir(cwd)
|
|
case "file URI with host and path":
|
|
expectedPath, _ := filepath.Abs(filepath.Join("hostname", "path"))
|
|
expected = "file://" + expectedPath
|
|
default:
|
|
expected = tt.expected
|
|
}
|
|
|
|
assert.Equal(t, expected, result.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPrepareRepoWithNormalization(t *testing.T) {
|
|
repoPath := setupTestRepo(t, "test-repo")
|
|
addTestFileAndCommit(t, repoPath, "test.txt", "test content")
|
|
|
|
originalDir, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("failed to get current directory: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := os.Chdir(originalDir); err != nil {
|
|
t.Fatalf("failed to restore original directory: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Test absolute paths first (without changing directory)
|
|
absoluteTests := []struct {
|
|
name string
|
|
uri string
|
|
trustLocalConfig bool
|
|
}{
|
|
{
|
|
name: "absolute file URI - no trust",
|
|
uri: "file://" + repoPath,
|
|
trustLocalConfig: false,
|
|
},
|
|
{
|
|
name: "absolute file URI - with trust",
|
|
uri: "file://" + repoPath,
|
|
trustLocalConfig: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range absoluteTests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
path, _, _ := PrepareRepo(ctx, tt.uri, "", tt.trustLocalConfig, false)
|
|
|
|
if !tt.trustLocalConfig {
|
|
assert.NotEqual(t, repoPath, path, "Cloned repo should not use original path")
|
|
cmd := exec.Command("git", "-C", path, "status")
|
|
err := cmd.Run()
|
|
assert.NoError(t, err, "Cloned repo should be a valid git repository")
|
|
} else {
|
|
assert.Equal(t, repoPath, path, "Trusted repo should use original path")
|
|
}
|
|
|
|
if path != repoPath && path != "" {
|
|
os.RemoveAll(path)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Now change to temp directory for relative path tests
|
|
if err := os.Chdir(repoPath); err != nil {
|
|
t.Fatalf("failed to change to temp directory: %v", err)
|
|
}
|
|
|
|
relativeTests := []struct {
|
|
name string
|
|
uri string
|
|
trustLocalConfig bool
|
|
}{
|
|
{
|
|
name: "relative file URI with current directory - no trust",
|
|
uri: "file://.",
|
|
trustLocalConfig: false,
|
|
},
|
|
{
|
|
name: "relative file URI with current directory - with trust",
|
|
uri: "file://.",
|
|
trustLocalConfig: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range relativeTests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
path, _, _ := PrepareRepo(ctx, tt.uri, "", tt.trustLocalConfig, false)
|
|
|
|
if !tt.trustLocalConfig {
|
|
assert.NotEqual(t, tt.uri, "file://"+path, "Cloned repo should not use original path")
|
|
cmd := exec.Command("git", "-C", path, "status")
|
|
err := cmd.Run()
|
|
assert.NoError(t, err, "Cloned repo should be a valid git repository")
|
|
} else {
|
|
assert.Equal(t, tt.uri, "file://"+path, "Trusted repo should use original path")
|
|
}
|
|
|
|
if path != repoPath && path != "" {
|
|
os.RemoveAll(path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPrepareRepoWithNormalizationBare(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
workingRepoPath := setupTestRepo(t, "working-repo-original")
|
|
addTestFileAndCommit(t, workingRepoPath, "test.txt", "test content")
|
|
|
|
tempDir := t.TempDir()
|
|
bareRepoPath := filepath.Join(tempDir, "bare-repo")
|
|
assert.NoError(t, exec.Command("git", "clone", workingRepoPath, bareRepoPath, "--bare").Run())
|
|
|
|
_, err := os.Stat(filepath.Join(bareRepoPath, "refs"))
|
|
assert.NoError(t, err, "Bare repository should have refs directory")
|
|
_, err = os.Stat(filepath.Join(bareRepoPath, "HEAD"))
|
|
assert.NoError(t, err, "Bare repository should have HEAD file")
|
|
|
|
originalDir, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("failed to get current directory: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := os.Chdir(originalDir); err != nil {
|
|
t.Fatalf("failed to restore original directory: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Test absolute paths first (without changing directory)
|
|
absoluteTests := []struct {
|
|
name string
|
|
uri string
|
|
trustLocalConfig bool
|
|
}{
|
|
{
|
|
name: "absolute file URI with bare repo - no trust",
|
|
uri: "file://" + bareRepoPath,
|
|
trustLocalConfig: false,
|
|
},
|
|
{
|
|
name: "absolute file URI with bare repo - with trust",
|
|
uri: "file://" + bareRepoPath,
|
|
trustLocalConfig: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range absoluteTests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
path, isRemote, err := PrepareRepo(ctx, tt.uri, "", tt.trustLocalConfig, true)
|
|
|
|
assert.NoError(t, err, "PrepareRepo should succeed")
|
|
assert.False(t, isRemote, "File URI should not be considered remote")
|
|
|
|
if tt.trustLocalConfig {
|
|
assert.Equal(t, bareRepoPath, path, "Trusted bare repo should use original path")
|
|
|
|
_, err = os.Stat(filepath.Join(path, "refs"))
|
|
assert.NoError(t, err, "Original bare repo should have refs directory")
|
|
_, err = os.Stat(filepath.Join(path, "HEAD"))
|
|
assert.NoError(t, err, "Original bare repo should have HEAD file")
|
|
} else {
|
|
assert.NotEqual(t, bareRepoPath, path, "Sanitized bare repo path should be different from original")
|
|
|
|
_, err = os.Stat(filepath.Join(path, "refs"))
|
|
assert.NoError(t, err, "Cloned bare repo should have refs directory")
|
|
_, err = os.Stat(filepath.Join(path, "HEAD"))
|
|
assert.NoError(t, err, "Cloned bare repo should have HEAD file")
|
|
_, err = os.Stat(filepath.Join(path, ".git"))
|
|
assert.True(t, os.IsNotExist(err), "Bare repo should not have .git subdirectory")
|
|
}
|
|
|
|
if path != bareRepoPath && path != "" {
|
|
os.RemoveAll(path)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Now change to temp directory for relative path tests
|
|
if err := os.Chdir(tempDir); err != nil {
|
|
t.Fatalf("failed to change to temp directory: %v", err)
|
|
}
|
|
|
|
relativeTests := []struct {
|
|
name string
|
|
uri string
|
|
trustLocalConfig bool
|
|
}{
|
|
{
|
|
name: "relative file URI with bare repo - no trust",
|
|
uri: "file://./bare-repo",
|
|
trustLocalConfig: false,
|
|
},
|
|
{
|
|
name: "relative file URI with bare repo - with trust",
|
|
uri: "file://./bare-repo",
|
|
trustLocalConfig: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range relativeTests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
path, isRemote, err := PrepareRepo(ctx, tt.uri, "", tt.trustLocalConfig, true)
|
|
|
|
assert.NoError(t, err, "PrepareRepo should succeed")
|
|
assert.False(t, isRemote, "File URI should not be considered remote")
|
|
|
|
if tt.trustLocalConfig {
|
|
assert.Equal(t, tt.uri, "file://"+path, "Trusted bare repo should use relative path")
|
|
|
|
_, err = os.Stat(filepath.Join(path, "refs"))
|
|
assert.NoError(t, err, "Original bare repo should have refs directory")
|
|
_, err = os.Stat(filepath.Join(path, "HEAD"))
|
|
assert.NoError(t, err, "Original bare repo should have HEAD file")
|
|
} else {
|
|
assert.NotEqual(t, tt.uri, "file://"+path, "Sanitized bare repo path should be different from original")
|
|
|
|
_, err = os.Stat(filepath.Join(path, "refs"))
|
|
assert.NoError(t, err, "Cloned bare repo should have refs directory")
|
|
_, err = os.Stat(filepath.Join(path, "HEAD"))
|
|
assert.NoError(t, err, "Cloned bare repo should have HEAD file")
|
|
_, err = os.Stat(filepath.Join(path, ".git"))
|
|
assert.True(t, os.IsNotExist(err), "Bare repo should not have .git subdirectory")
|
|
}
|
|
|
|
if path != bareRepoPath && path != "" {
|
|
os.RemoveAll(path)
|
|
}
|
|
})
|
|
}
|
|
}
|