2021-09-09 20:13:36 +00:00
// Copyright 2021 The Gitea Authors. All rights reserved.
2022-11-27 18:20:29 +00:00
// SPDX-License-Identifier: MIT
2021-09-09 20:13:36 +00:00
package git
import (
2024-04-01 06:44:46 +00:00
"context"
"fmt"
"io"
"io/fs"
"os"
2024-03-24 11:44:30 +00:00
"path/filepath"
2024-04-01 06:44:46 +00:00
"runtime"
"strings"
2021-09-09 20:13:36 +00:00
"testing"
"time"
2024-03-24 11:44:30 +00:00
"code.gitea.io/gitea/modules/test"
2021-09-09 20:13:36 +00:00
"github.com/stretchr/testify/assert"
2024-03-24 11:44:30 +00:00
"github.com/stretchr/testify/require"
2021-09-09 20:13:36 +00:00
)
2024-04-01 06:44:46 +00:00
func TestNewCheckAttrStdoutReader ( t * testing . T ) {
t . Run ( "two_times" , func ( t * testing . T ) {
read := newCheckAttrStdoutReader ( strings . NewReader (
".gitignore\x00linguist-vendored\x00unspecified\x00" +
".gitignore\x00linguist-vendored\x00specified" ,
) , 1 )
2021-09-09 20:13:36 +00:00
2024-04-01 06:44:46 +00:00
// first read
attr , err := read ( )
assert . NoError ( t , err )
assert . Equal ( t , map [ string ] GitAttribute {
"linguist-vendored" : GitAttribute ( "unspecified" ) ,
} , attr )
2021-09-09 20:13:36 +00:00
2024-04-01 06:44:46 +00:00
// second read
attr , err = read ( )
assert . NoError ( t , err )
assert . Equal ( t , map [ string ] GitAttribute {
"linguist-vendored" : GitAttribute ( "specified" ) ,
} , attr )
} )
t . Run ( "incomplete" , func ( t * testing . T ) {
read := newCheckAttrStdoutReader ( strings . NewReader (
"filename\x00linguist-vendored" ,
) , 1 )
2021-09-09 20:13:36 +00:00
2024-04-01 06:44:46 +00:00
_ , err := read ( )
assert . Equal ( t , io . ErrUnexpectedEOF , err )
} )
t . Run ( "three_times" , func ( t * testing . T ) {
read := newCheckAttrStdoutReader ( strings . NewReader (
"shouldbe.vendor\x00linguist-vendored\x00set\x00" +
"shouldbe.vendor\x00linguist-generated\x00unspecified\x00" +
"shouldbe.vendor\x00linguist-language\x00unspecified\x00" ,
) , 1 )
// first read
attr , err := read ( )
assert . NoError ( t , err )
assert . Equal ( t , map [ string ] GitAttribute {
"linguist-vendored" : GitAttribute ( "set" ) ,
} , attr )
2021-09-09 20:13:36 +00:00
2024-04-01 06:44:46 +00:00
// second read
attr , err = read ( )
assert . NoError ( t , err )
assert . Equal ( t , map [ string ] GitAttribute {
"linguist-generated" : GitAttribute ( "unspecified" ) ,
} , attr )
2021-09-09 20:13:36 +00:00
2024-04-01 06:44:46 +00:00
// third read
attr , err = read ( )
assert . NoError ( t , err )
assert . Equal ( t , map [ string ] GitAttribute {
"linguist-language" : GitAttribute ( "unspecified" ) ,
} , attr )
} )
2021-09-09 20:13:36 +00:00
}
2024-03-24 11:44:30 +00:00
func TestGitAttributeBareNonBare ( t * testing . T ) {
if ! SupportCheckAttrOnBare {
t . Skip ( "git check-attr supported on bare repo starting with git 2.40" )
}
repoPath := filepath . Join ( testReposDir , "language_stats_repo" )
gitRepo , err := openRepositoryWithDefaultContext ( repoPath )
require . NoError ( t , err )
defer gitRepo . Close ( )
for _ , commitID := range [ ] string {
"8fee858da5796dfb37704761701bb8e800ad9ef3" ,
"341fca5b5ea3de596dc483e54c2db28633cd2f97" ,
} {
2024-04-01 06:44:46 +00:00
bareStats , err := gitRepo . GitAttributes ( commitID , "i-am-a-python.p" , LinguistAttributes ... )
assert . NoError ( t , err )
defer test . MockVariableValue ( & SupportCheckAttrOnBare , false ) ( )
cloneStats , err := gitRepo . GitAttributes ( commitID , "i-am-a-python.p" , LinguistAttributes ... )
assert . NoError ( t , err )
assert . EqualValues ( t , cloneStats , bareStats )
refStats := cloneStats
t . Run ( "GitAttributeChecker/" + commitID + "/SupportBare" , func ( t * testing . T ) {
2024-03-24 11:44:30 +00:00
bareChecker , err := gitRepo . GitAttributeChecker ( commitID , LinguistAttributes ... )
assert . NoError ( t , err )
2024-04-01 06:44:46 +00:00
defer bareChecker . Close ( )
2024-03-24 11:44:30 +00:00
bareStats , err := bareChecker . CheckPath ( "i-am-a-python.p" )
assert . NoError ( t , err )
2024-04-01 06:44:46 +00:00
assert . EqualValues ( t , refStats , bareStats )
} )
t . Run ( "GitAttributeChecker/" + commitID + "/NoBareSupport" , func ( t * testing . T ) {
2024-03-24 11:44:30 +00:00
defer test . MockVariableValue ( & SupportCheckAttrOnBare , false ) ( )
cloneChecker , err := gitRepo . GitAttributeChecker ( commitID , LinguistAttributes ... )
assert . NoError ( t , err )
2024-04-01 06:44:46 +00:00
defer cloneChecker . Close ( )
2024-03-24 11:44:30 +00:00
2024-04-01 06:44:46 +00:00
cloneStats , err := cloneChecker . CheckPath ( "i-am-a-python.p" )
2024-03-24 11:44:30 +00:00
assert . NoError ( t , err )
2024-04-01 06:44:46 +00:00
assert . EqualValues ( t , refStats , cloneStats )
2024-03-24 11:44:30 +00:00
} )
}
}
func TestGitAttributes ( t * testing . T ) {
repoPath := filepath . Join ( testReposDir , "language_stats_repo" )
gitRepo , err := openRepositoryWithDefaultContext ( repoPath )
require . NoError ( t , err )
defer gitRepo . Close ( )
attr , err := gitRepo . GitAttributes ( "8fee858da5796dfb37704761701bb8e800ad9ef3" , "i-am-a-python.p" , LinguistAttributes ... )
assert . NoError ( t , err )
assert . EqualValues ( t , map [ string ] GitAttribute {
"gitlab-language" : "unspecified" ,
"linguist-detectable" : "unspecified" ,
"linguist-documentation" : "unspecified" ,
"linguist-generated" : "unspecified" ,
"linguist-language" : "Python" ,
"linguist-vendored" : "unspecified" ,
} , attr )
attr , err = gitRepo . GitAttributes ( "341fca5b5ea3de596dc483e54c2db28633cd2f97" , "i-am-a-python.p" , LinguistAttributes ... )
assert . NoError ( t , err )
assert . EqualValues ( t , map [ string ] GitAttribute {
"gitlab-language" : "unspecified" ,
"linguist-detectable" : "unspecified" ,
"linguist-documentation" : "unspecified" ,
"linguist-generated" : "unspecified" ,
"linguist-language" : "Cobra" ,
"linguist-vendored" : "unspecified" ,
} , attr )
}
func TestGitAttributeFirst ( t * testing . T ) {
repoPath := filepath . Join ( testReposDir , "language_stats_repo" )
gitRepo , err := openRepositoryWithDefaultContext ( repoPath )
require . NoError ( t , err )
defer gitRepo . Close ( )
t . Run ( "first is specified" , func ( t * testing . T ) {
language , err := gitRepo . GitAttributeFirst ( "8fee858da5796dfb37704761701bb8e800ad9ef3" , "i-am-a-python.p" , "linguist-language" , "gitlab-language" )
assert . NoError ( t , err )
assert . Equal ( t , "Python" , language . String ( ) )
} )
t . Run ( "second is specified" , func ( t * testing . T ) {
language , err := gitRepo . GitAttributeFirst ( "8fee858da5796dfb37704761701bb8e800ad9ef3" , "i-am-a-python.p" , "gitlab-language" , "linguist-language" )
assert . NoError ( t , err )
assert . Equal ( t , "Python" , language . String ( ) )
} )
t . Run ( "none is specified" , func ( t * testing . T ) {
language , err := gitRepo . GitAttributeFirst ( "8fee858da5796dfb37704761701bb8e800ad9ef3" , "i-am-a-python.p" , "linguist-detectable" , "gitlab-language" , "non-existing" )
assert . NoError ( t , err )
assert . Equal ( t , "" , language . String ( ) )
} )
}
func TestGitAttributeStruct ( t * testing . T ) {
assert . Equal ( t , "" , GitAttribute ( "" ) . String ( ) )
assert . Equal ( t , "" , GitAttribute ( "unspecified" ) . String ( ) )
assert . Equal ( t , "python" , GitAttribute ( "python" ) . String ( ) )
assert . Equal ( t , "text?token=Error" , GitAttribute ( "text?token=Error" ) . String ( ) )
assert . Equal ( t , "text" , GitAttribute ( "text?token=Error" ) . Prefix ( ) )
}
2024-04-01 06:44:46 +00:00
func TestGitAttributeCheckerError ( t * testing . T ) {
prepareRepo := func ( t * testing . T ) * Repository {
t . Helper ( )
path := t . TempDir ( )
// we can't use unittest.CopyDir because of an import cycle (git.Init in unittest)
require . NoError ( t , CopyFS ( path , os . DirFS ( filepath . Join ( testReposDir , "language_stats_repo" ) ) ) )
gitRepo , err := openRepositoryWithDefaultContext ( path )
require . NoError ( t , err )
return gitRepo
}
t . Run ( "RemoveAll/BeforeRun" , func ( t * testing . T ) {
gitRepo := prepareRepo ( t )
defer gitRepo . Close ( )
assert . NoError ( t , os . RemoveAll ( gitRepo . Path ) )
ac , err := gitRepo . GitAttributeChecker ( "" , "linguist-language" )
require . NoError ( t , err )
_ , err = ac . CheckPath ( "i-am-a-python.p" )
assert . Error ( t , err )
assert . Contains ( t , err . Error ( ) , ` git check-attr (stderr: ""): ` )
} )
t . Run ( "RemoveAll/DuringRun" , func ( t * testing . T ) {
gitRepo := prepareRepo ( t )
defer gitRepo . Close ( )
ac , err := gitRepo . GitAttributeChecker ( "" , "linguist-language" )
require . NoError ( t , err )
// calling CheckPath before would allow git to cache part of it and succesfully return later
assert . NoError ( t , os . RemoveAll ( gitRepo . Path ) )
_ , err = ac . CheckPath ( "i-am-a-python.p" )
2024-04-02 07:00:33 +00:00
if err == nil {
t . Skip (
"git check-attr started too fast and CheckPath was succesful (and likely cached)" ,
"https://codeberg.org/forgejo/forgejo/issues/2948" ,
)
}
2024-04-01 06:44:46 +00:00
// Depending on the order of execution, the returned error can be:
// - a launch error "fork/exec /usr/bin/git: no such file or directory" (when the removal happens before the Run)
// - a git error (stderr: "fatal: Unable to read current working directory: No such file or directory"): exit status 128 (when the removal happens after the Run)
// (pipe error "write |1: broken pipe" should be replaced by one of the Run errors above)
assert . Contains ( t , err . Error ( ) , ` git check-attr ` )
} )
t . Run ( "Cancelled/BeforeRun" , func ( t * testing . T ) {
gitRepo := prepareRepo ( t )
defer gitRepo . Close ( )
var cancel context . CancelFunc
gitRepo . Ctx , cancel = context . WithCancel ( gitRepo . Ctx )
cancel ( )
ac , err := gitRepo . GitAttributeChecker ( "8fee858da5796dfb37704761701bb8e800ad9ef3" , "linguist-language" )
require . NoError ( t , err )
_ , err = ac . CheckPath ( "i-am-a-python.p" )
assert . ErrorIs ( t , err , context . Canceled )
} )
t . Run ( "Cancelled/DuringRun" , func ( t * testing . T ) {
gitRepo := prepareRepo ( t )
defer gitRepo . Close ( )
var cancel context . CancelFunc
gitRepo . Ctx , cancel = context . WithCancel ( gitRepo . Ctx )
ac , err := gitRepo . GitAttributeChecker ( "8fee858da5796dfb37704761701bb8e800ad9ef3" , "linguist-language" )
require . NoError ( t , err )
attr , err := ac . CheckPath ( "i-am-a-python.p" )
assert . NoError ( t , err )
assert . Equal ( t , "Python" , attr [ "linguist-language" ] . String ( ) )
errCh := make ( chan error )
go func ( ) {
cancel ( )
for err == nil {
_ , err = ac . CheckPath ( "i-am-a-python.p" )
runtime . Gosched ( ) // the cancellation must have time to propagate
}
errCh <- err
} ( )
select {
case <- time . After ( time . Second ) :
t . Error ( "CheckPath did not complete within 1s" )
case err = <- errCh :
assert . ErrorIs ( t , err , context . Canceled )
}
} )
t . Run ( "Closed/BeforeRun" , func ( t * testing . T ) {
gitRepo := prepareRepo ( t )
defer gitRepo . Close ( )
ac , err := gitRepo . GitAttributeChecker ( "8fee858da5796dfb37704761701bb8e800ad9ef3" , "linguist-language" )
require . NoError ( t , err )
assert . NoError ( t , ac . Close ( ) )
_ , err = ac . CheckPath ( "i-am-a-python.p" )
assert . ErrorIs ( t , err , fs . ErrClosed )
} )
t . Run ( "Closed/DuringRun" , func ( t * testing . T ) {
gitRepo := prepareRepo ( t )
defer gitRepo . Close ( )
ac , err := gitRepo . GitAttributeChecker ( "8fee858da5796dfb37704761701bb8e800ad9ef3" , "linguist-language" )
require . NoError ( t , err )
attr , err := ac . CheckPath ( "i-am-a-python.p" )
assert . NoError ( t , err )
assert . Equal ( t , "Python" , attr [ "linguist-language" ] . String ( ) )
assert . NoError ( t , ac . Close ( ) )
_ , err = ac . CheckPath ( "i-am-a-python.p" )
assert . ErrorIs ( t , err , fs . ErrClosed )
} )
}
// CopyFS is adapted from https://github.com/golang/go/issues/62484
// which should be available with go1.23
func CopyFS ( dir string , fsys fs . FS ) error {
return fs . WalkDir ( fsys , "." , func ( path string , d fs . DirEntry , _ error ) error {
targ := filepath . Join ( dir , filepath . FromSlash ( path ) )
if d . IsDir ( ) {
return os . MkdirAll ( targ , 0 o777 )
}
r , err := fsys . Open ( path )
if err != nil {
return err
}
defer r . Close ( )
info , err := r . Stat ( )
if err != nil {
return err
}
w , err := os . OpenFile ( targ , os . O_CREATE | os . O_TRUNC | os . O_WRONLY , 0 o666 | info . Mode ( ) & 0 o777 )
if err != nil {
return err
}
if _ , err := io . Copy ( w , r ) ; err != nil {
w . Close ( )
return fmt . Errorf ( "copying %s: %v" , path , err )
}
return w . Close ( )
} )
}