diff options
-rw-r--r-- | .github/workflows/test.yml | 3 | ||||
-rw-r--r-- | go.mod | 12 | ||||
-rw-r--r-- | go.sum | 26 | ||||
-rw-r--r-- | plumbing/format/gitignore/dir.go | 4 | ||||
-rw-r--r-- | plumbing/format/gitignore/dir_test.go | 16 | ||||
-rw-r--r-- | plumbing/format/packfile/delta_index.go | 20 | ||||
-rw-r--r-- | plumbing/transport/common.go | 7 | ||||
-rw-r--r-- | plumbing/transport/common_test.go | 35 | ||||
-rw-r--r-- | plumbing/transport/ssh/auth_method.go | 13 | ||||
-rw-r--r-- | plumbing/transport/ssh/auth_method_test.go | 106 | ||||
-rw-r--r-- | plumbing/transport/ssh/common.go | 17 | ||||
-rw-r--r-- | remote.go | 27 | ||||
-rw-r--r-- | remote_test.go | 44 | ||||
-rw-r--r-- | status.go | 69 | ||||
-rw-r--r-- | storage/filesystem/dotgit/dotgit.go | 18 | ||||
-rw-r--r-- | storage/filesystem/dotgit/dotgit_test.go | 61 | ||||
-rw-r--r-- | storage/filesystem/object.go | 4 | ||||
-rw-r--r-- | storage/filesystem/object_test.go | 61 | ||||
-rw-r--r-- | utils/merkletrie/change.go | 9 | ||||
-rw-r--r-- | utils/merkletrie/change_test.go | 11 | ||||
-rw-r--r-- | worktree_status.go | 30 | ||||
-rw-r--r-- | worktree_status_test.go | 89 | ||||
-rw-r--r-- | worktree_test.go | 27 |
23 files changed, 638 insertions, 71 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 96090c0..a04763d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,3 +34,6 @@ jobs: - name: Test run: make test-coverage + + - name: Test Examples + run: go test -timeout 30s -v -run '^TestExamples$' github.com/go-git/go-git/v5/_examples --examples @@ -19,13 +19,13 @@ require ( github.com/kevinburke/ssh_config v1.2.0 github.com/pjbgf/sha1cd v0.3.0 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 - github.com/skeema/knownhosts v1.2.2 + github.com/skeema/knownhosts v1.3.0 github.com/stretchr/testify v1.9.0 github.com/xanzy/ssh-agent v0.3.3 - golang.org/x/crypto v0.24.0 - golang.org/x/net v0.26.0 - golang.org/x/sys v0.21.0 - golang.org/x/text v0.16.0 + golang.org/x/crypto v0.26.0 + golang.org/x/net v0.28.0 + golang.org/x/sys v0.23.0 + golang.org/x/text v0.17.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c ) @@ -40,7 +40,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/sync v0.7.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -64,8 +64,8 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= -github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= +github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -79,8 +79,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= @@ -92,13 +92,13 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -112,14 +112,14 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -127,8 +127,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/plumbing/format/gitignore/dir.go b/plumbing/format/gitignore/dir.go index aca5d0d..92df5a3 100644 --- a/plumbing/format/gitignore/dir.go +++ b/plumbing/format/gitignore/dir.go @@ -64,6 +64,10 @@ func ReadPatterns(fs billy.Filesystem, path []string) (ps []Pattern, err error) for _, fi := range fis { if fi.IsDir() && fi.Name() != gitDir { + if NewMatcher(ps).Match(append(path, fi.Name()), true) { + continue + } + var subps []Pattern subps, err = ReadPatterns(fs, append(path, fi.Name())) if err != nil { diff --git a/plumbing/format/gitignore/dir_test.go b/plumbing/format/gitignore/dir_test.go index 465c571..ba8ad80 100644 --- a/plumbing/format/gitignore/dir_test.go +++ b/plumbing/format/gitignore/dir_test.go @@ -44,6 +44,8 @@ func (s *MatcherSuite) SetUpTest(c *C) { c.Assert(err, IsNil) _, err = f.Write([]byte("ignore.crlf\r\n")) c.Assert(err, IsNil) + _, err = f.Write([]byte("ignore_dir\n")) + c.Assert(err, IsNil) err = f.Close() c.Assert(err, IsNil) @@ -56,6 +58,17 @@ func (s *MatcherSuite) SetUpTest(c *C) { err = f.Close() c.Assert(err, IsNil) + err = fs.MkdirAll("ignore_dir", os.ModePerm) + c.Assert(err, IsNil) + f, err = fs.Create("ignore_dir/.gitignore") + c.Assert(err, IsNil) + _, err = f.Write([]byte("!file\n")) + c.Assert(err, IsNil) + _, err = fs.Create("ignore_dir/file") + c.Assert(err, IsNil) + err = f.Close() + c.Assert(err, IsNil) + err = fs.MkdirAll("another", os.ModePerm) c.Assert(err, IsNil) err = fs.MkdirAll("exclude.crlf", os.ModePerm) @@ -267,12 +280,13 @@ func (s *MatcherSuite) SetUpTest(c *C) { func (s *MatcherSuite) TestDir_ReadPatterns(c *C) { checkPatterns := func(ps []Pattern) { - c.Assert(ps, HasLen, 6) + c.Assert(ps, HasLen, 7) m := NewMatcher(ps) c.Assert(m.Match([]string{"exclude.crlf"}, true), Equals, true) c.Assert(m.Match([]string{"ignore.crlf"}, true), Equals, true) c.Assert(m.Match([]string{"vendor", "gopkg.in"}, true), Equals, true) + c.Assert(m.Match([]string{"ignore_dir", "file"}, false), Equals, true) c.Assert(m.Match([]string{"vendor", "github.com"}, true), Equals, false) c.Assert(m.Match([]string{"multiple", "sub", "ignores", "first", "ignore_dir"}, true), Equals, true) c.Assert(m.Match([]string{"multiple", "sub", "ignores", "second", "ignore_dir"}, true), Equals, true) diff --git a/plumbing/format/packfile/delta_index.go b/plumbing/format/packfile/delta_index.go index 07a6112..a60ec0b 100644 --- a/plumbing/format/packfile/delta_index.go +++ b/plumbing/format/packfile/delta_index.go @@ -32,19 +32,17 @@ func (idx *deltaIndex) findMatch(src, tgt []byte, tgtOffset int) (srcOffset, l i return 0, -1 } - if len(tgt) >= tgtOffset+s && len(src) >= blksz { - h := hashBlock(tgt, tgtOffset) - tIdx := h & idx.mask - eIdx := idx.table[tIdx] - if eIdx != 0 { - srcOffset = idx.entries[eIdx] - } else { - return - } - - l = matchLength(src, tgt, tgtOffset, srcOffset) + h := hashBlock(tgt, tgtOffset) + tIdx := h & idx.mask + eIdx := idx.table[tIdx] + if eIdx == 0 { + return } + srcOffset = idx.entries[eIdx] + + l = matchLength(src, tgt, tgtOffset, srcOffset) + return } diff --git a/plumbing/transport/common.go b/plumbing/transport/common.go index b05437f..fae1aa9 100644 --- a/plumbing/transport/common.go +++ b/plumbing/transport/common.go @@ -19,6 +19,7 @@ import ( "fmt" "io" "net/url" + "path/filepath" "strconv" "strings" @@ -295,7 +296,11 @@ func parseFile(endpoint string) (*Endpoint, bool) { return nil, false } - path := endpoint + path, err := filepath.Abs(endpoint) + if err != nil { + return nil, false + } + return &Endpoint{ Protocol: "file", Path: path, diff --git a/plumbing/transport/common_test.go b/plumbing/transport/common_test.go index 3efc555..1501f73 100644 --- a/plumbing/transport/common_test.go +++ b/plumbing/transport/common_test.go @@ -3,6 +3,9 @@ package transport import ( "fmt" "net/url" + "os" + "path/filepath" + "runtime" "testing" "github.com/go-git/go-git/v5/plumbing/protocol/packp/capability" @@ -120,6 +123,14 @@ func (s *SuiteCommon) TestNewEndpointSCPLikeWithPort(c *C) { } func (s *SuiteCommon) TestNewEndpointFileAbs(c *C) { + var err error + abs := "/foo.git" + + if runtime.GOOS == "windows" { + abs, err = filepath.Abs(abs) + c.Assert(err, IsNil) + } + e, err := NewEndpoint("/foo.git") c.Assert(err, IsNil) c.Assert(e.Protocol, Equals, "file") @@ -127,11 +138,14 @@ func (s *SuiteCommon) TestNewEndpointFileAbs(c *C) { c.Assert(e.Password, Equals, "") c.Assert(e.Host, Equals, "") c.Assert(e.Port, Equals, 0) - c.Assert(e.Path, Equals, "/foo.git") - c.Assert(e.String(), Equals, "file:///foo.git") + c.Assert(e.Path, Equals, abs) + c.Assert(e.String(), Equals, "file://"+abs) } func (s *SuiteCommon) TestNewEndpointFileRel(c *C) { + abs, err := filepath.Abs("foo.git") + c.Assert(err, IsNil) + e, err := NewEndpoint("foo.git") c.Assert(err, IsNil) c.Assert(e.Protocol, Equals, "file") @@ -139,11 +153,20 @@ func (s *SuiteCommon) TestNewEndpointFileRel(c *C) { c.Assert(e.Password, Equals, "") c.Assert(e.Host, Equals, "") c.Assert(e.Port, Equals, 0) - c.Assert(e.Path, Equals, "foo.git") - c.Assert(e.String(), Equals, "file://foo.git") + c.Assert(e.Path, Equals, abs) + c.Assert(e.String(), Equals, "file://"+abs) } func (s *SuiteCommon) TestNewEndpointFileWindows(c *C) { + abs := "C:\\foo.git" + + if runtime.GOOS != "windows" { + cwd, err := os.Getwd() + c.Assert(err, IsNil) + + abs = filepath.Join(cwd, "C:\\foo.git") + } + e, err := NewEndpoint("C:\\foo.git") c.Assert(err, IsNil) c.Assert(e.Protocol, Equals, "file") @@ -151,8 +174,8 @@ func (s *SuiteCommon) TestNewEndpointFileWindows(c *C) { c.Assert(e.Password, Equals, "") c.Assert(e.Host, Equals, "") c.Assert(e.Port, Equals, 0) - c.Assert(e.Path, Equals, "C:\\foo.git") - c.Assert(e.String(), Equals, "file://C:\\foo.git") + c.Assert(e.Path, Equals, abs) + c.Assert(e.String(), Equals, "file://"+abs) } func (s *SuiteCommon) TestNewEndpointFileURL(c *C) { diff --git a/plumbing/transport/ssh/auth_method.go b/plumbing/transport/ssh/auth_method.go index ac4e358..f9c598e 100644 --- a/plumbing/transport/ssh/auth_method.go +++ b/plumbing/transport/ssh/auth_method.go @@ -230,11 +230,11 @@ func (a *PublicKeysCallback) ClientConfig() (*ssh.ClientConfig, error) { // ~/.ssh/known_hosts // /etc/ssh/ssh_known_hosts func NewKnownHostsCallback(files ...string) (ssh.HostKeyCallback, error) { - kh, err := newKnownHosts(files...) - return ssh.HostKeyCallback(kh), err + db, err := newKnownHostsDb(files...) + return db.HostKeyCallback(), err } -func newKnownHosts(files ...string) (knownhosts.HostKeyCallback, error) { +func newKnownHostsDb(files ...string) (*knownhosts.HostKeyDB, error) { var err error if len(files) == 0 { @@ -247,7 +247,7 @@ func newKnownHosts(files ...string) (knownhosts.HostKeyCallback, error) { return nil, err } - return knownhosts.New(files...) + return knownhosts.NewDB(files...) } func getDefaultKnownHostsFiles() ([]string, error) { @@ -301,11 +301,12 @@ type HostKeyCallbackHelper struct { // HostKeyCallback is empty a default callback is created using // NewKnownHostsCallback. func (m *HostKeyCallbackHelper) SetHostKeyCallback(cfg *ssh.ClientConfig) (*ssh.ClientConfig, error) { - var err error if m.HostKeyCallback == nil { - if m.HostKeyCallback, err = NewKnownHostsCallback(); err != nil { + db, err := newKnownHostsDb() + if err != nil { return cfg, err } + m.HostKeyCallback = db.HostKeyCallback() } cfg.HostKeyCallback = m.HostKeyCallback diff --git a/plumbing/transport/ssh/auth_method_test.go b/plumbing/transport/ssh/auth_method_test.go index b275018..e3f652e 100644 --- a/plumbing/transport/ssh/auth_method_test.go +++ b/plumbing/transport/ssh/auth_method_test.go @@ -18,7 +18,8 @@ import ( type ( SuiteCommon struct{} - mockKnownHosts struct{} + mockKnownHosts struct{} + mockKnownHostsWithCert struct{} ) func (mockKnownHosts) host() string { return "github.com" } @@ -27,6 +28,19 @@ func (mockKnownHosts) knownHosts() []byte { } func (mockKnownHosts) Network() string { return "tcp" } func (mockKnownHosts) String() string { return "github.com:22" } +func (mockKnownHosts) Algorithms() []string { + return []string{ssh.KeyAlgoRSA, ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512} +} + +func (mockKnownHostsWithCert) host() string { return "github.com" } +func (mockKnownHostsWithCert) knownHosts() []byte { + return []byte(`@cert-authority github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`) +} +func (mockKnownHostsWithCert) Network() string { return "tcp" } +func (mockKnownHostsWithCert) String() string { return "github.com:22" } +func (mockKnownHostsWithCert) Algorithms() []string { + return []string{ssh.CertAlgoRSASHA512v01, ssh.CertAlgoRSASHA256v01, ssh.CertAlgoRSAv01} +} var _ = Suite(&SuiteCommon{}) @@ -230,3 +244,93 @@ func (*SuiteCommon) TestNewKnownHostsCallback(c *C) { err = clb(mock.String(), mock, hostKey) c.Assert(err, IsNil) } + +func (*SuiteCommon) TestNewKnownHostsDbWithoutCert(c *C) { + if runtime.GOOS == "js" { + c.Skip("not available in wasm") + } + + var mock = mockKnownHosts{} + + f, err := util.TempFile(osfs.Default, "", "known-hosts") + c.Assert(err, IsNil) + + _, err = f.Write(mock.knownHosts()) + c.Assert(err, IsNil) + + err = f.Close() + c.Assert(err, IsNil) + + defer util.RemoveAll(osfs.Default, f.Name()) + + f, err = osfs.Default.Open(f.Name()) + c.Assert(err, IsNil) + + defer f.Close() + + db, err := newKnownHostsDb(f.Name()) + c.Assert(err, IsNil) + + algos := db.HostKeyAlgorithms(mock.String()) + c.Assert(algos, HasLen, len(mock.Algorithms())) + + contains := func(container []string, value string) bool { + for _, inner := range container { + if inner == value { + return true + } + } + return false + } + + for _, algorithm := range mock.Algorithms() { + if !contains(algos, algorithm) { + c.Error("algos does not contain ", algorithm) + } + } +} + +func (*SuiteCommon) TestNewKnownHostsDbWithCert(c *C) { + if runtime.GOOS == "js" { + c.Skip("not available in wasm") + } + + var mock = mockKnownHostsWithCert{} + + f, err := util.TempFile(osfs.Default, "", "known-hosts") + c.Assert(err, IsNil) + + _, err = f.Write(mock.knownHosts()) + c.Assert(err, IsNil) + + err = f.Close() + c.Assert(err, IsNil) + + defer util.RemoveAll(osfs.Default, f.Name()) + + f, err = osfs.Default.Open(f.Name()) + c.Assert(err, IsNil) + + defer f.Close() + + db, err := newKnownHostsDb(f.Name()) + c.Assert(err, IsNil) + + algos := db.HostKeyAlgorithms(mock.String()) + c.Assert(algos, HasLen, len(mock.Algorithms())) + + contains := func(container []string, value string) bool { + for _, inner := range container { + if inner == value { + return true + } + } + return false + } + + for _, algorithm := range mock.Algorithms() { + if !contains(algos, algorithm) { + c.Error("algos does not contain ", algorithm) + } + } +} diff --git a/plumbing/transport/ssh/common.go b/plumbing/transport/ssh/common.go index 05dea44..a37024f 100644 --- a/plumbing/transport/ssh/common.go +++ b/plumbing/transport/ssh/common.go @@ -11,7 +11,6 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/internal/common" - "github.com/skeema/knownhosts" "github.com/kevinburke/ssh_config" "golang.org/x/crypto/ssh" @@ -127,17 +126,25 @@ func (c *command) connect() error { } hostWithPort := c.getHostWithPort() if config.HostKeyCallback == nil { - kh, err := newKnownHosts() + db, err := newKnownHostsDb() if err != nil { return err } - config.HostKeyCallback = kh.HostKeyCallback() - config.HostKeyAlgorithms = kh.HostKeyAlgorithms(hostWithPort) + + config.HostKeyCallback = db.HostKeyCallback() + config.HostKeyAlgorithms = db.HostKeyAlgorithms(hostWithPort) } else if len(config.HostKeyAlgorithms) == 0 { // Set the HostKeyAlgorithms based on HostKeyCallback. // For background see https://github.com/go-git/go-git/issues/411 as well as // https://github.com/golang/go/issues/29286 for root cause. - config.HostKeyAlgorithms = knownhosts.HostKeyAlgorithms(config.HostKeyCallback, hostWithPort) + db, err := newKnownHostsDb() + if err != nil { + return err + } + + // Note that the knownhost database is used, as it provides additional functionality + // to handle ssh cert-authorities. + config.HostKeyAlgorithms = db.HostKeyAlgorithms(hostWithPort) } overrideConfig(c.config, config) @@ -9,6 +9,7 @@ import ( "time" "github.com/go-git/go-billy/v5/osfs" + "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/internal/url" "github.com/go-git/go-git/v5/plumbing" @@ -491,7 +492,18 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen } if !updated && !updatedPrune { - return remoteRefs, NoErrAlreadyUpToDate + // No references updated, but may have fetched new objects, check if we now have any of our wants + for _, hash := range req.Wants { + exists, _ := objectExists(r.s, hash) + if exists { + updated = true + break + } + } + + if !updated { + return remoteRefs, NoErrAlreadyUpToDate + } } return remoteRefs, nil @@ -878,17 +890,12 @@ func getHavesFromRef( return nil } - // No need to load the commit if we know the remote already - // has this hash. - if remoteRefs[h] { - haves[h] = true - return nil - } - commit, err := object.GetCommit(s, h) if err != nil { - // Ignore the error if this isn't a commit. - haves[ref.Hash()] = true + if !errors.Is(err, plumbing.ErrObjectNotFound) { + // Ignore the error if this isn't a commit. + haves[ref.Hash()] = true + } return nil } diff --git a/remote_test.go b/remote_test.go index d1439d5..c816cc5 100644 --- a/remote_test.go +++ b/remote_test.go @@ -14,6 +14,9 @@ import ( "time" "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-billy/v5/osfs" + "github.com/go-git/go-billy/v5/util" + "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/cache" @@ -346,6 +349,38 @@ func (s *RemoteSuite) testFetch(c *C, r *Remote, o *FetchOptions, expected []*pl } } +func (s *RemoteSuite) TestFetchOfMissingObjects(c *C) { + tmp, clean := s.TemporalDir() + defer clean() + + // clone to a local temp folder + _, err := PlainClone(tmp, true, &CloneOptions{ + URL: fixtures.Basic().One().DotGit().Root(), + }) + c.Assert(err, IsNil) + + // Delete the pack files + fsTmp := osfs.New(tmp) + err = util.RemoveAll(fsTmp, "objects/pack") + c.Assert(err, IsNil) + + // Reopen the repo from the filesystem (with missing objects) + r, err := Open(filesystem.NewStorage(fsTmp, cache.NewObjectLRUDefault()), nil) + c.Assert(err, IsNil) + + // Confirm we are missing a commit + _, err = r.CommitObject(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) + c.Assert(err, Equals, plumbing.ErrObjectNotFound) + + // Refetch to get all the missing objects + err = r.Fetch(&FetchOptions{}) + c.Assert(err, IsNil) + + // Confirm we now have the commit + _, err = r.CommitObject(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) + c.Assert(err, IsNil) +} + func (s *RemoteSuite) TestFetchWithProgress(c *C) { url := s.GetBasicLocalRepositoryURL() sto := memory.NewStorage() @@ -1220,17 +1255,20 @@ func (s *RemoteSuite) TestGetHaves(c *C) { sto := filesystem.NewStorage(f.DotGit(), cache.NewObjectLRUDefault()) var localRefs = []*plumbing.Reference{ + // Exists plumbing.NewReferenceFromStrings( "foo", - "f7b877701fbf855b44c0a9e86f3fdce2c298b07f", + "b029517f6300c2da0f4b651b8642506cd6aaf45d", ), + // Exists plumbing.NewReferenceFromStrings( "bar", - "fe6cb94756faa81e5ed9240f9191b833db5f40ae", + "b8e471f58bcbca63b07bda20e428190409c2db47", ), + // Doesn't Exist plumbing.NewReferenceFromStrings( "qux", - "f7b877701fbf855b44c0a9e86f3fdce2c298b07f", + "0000000", ), } @@ -4,6 +4,9 @@ import ( "bytes" "fmt" "path/filepath" + + mindex "github.com/go-git/go-git/v5/utils/merkletrie/index" + "github.com/go-git/go-git/v5/utils/merkletrie/noder" ) // Status represents the current status of a Worktree. @@ -77,3 +80,69 @@ const ( Copied StatusCode = 'C' UpdatedButUnmerged StatusCode = 'U' ) + +// StatusStrategy defines the different types of strategies when processing +// the worktree status. +type StatusStrategy int + +const ( + // TODO: (V6) Review the default status strategy. + // TODO: (V6) Review the type used to represent Status, to enable lazy + // processing of statuses going direct to the backing filesystem. + defaultStatusStrategy = Empty + + // Empty starts its status map from empty. Missing entries for a given + // path means that the file is untracked. This causes a known issue (#119) + // whereby unmodified files can be incorrectly reported as untracked. + // + // This can be used when returning the changed state within a modified Worktree. + // For example, to check whether the current worktree is clean. + Empty StatusStrategy = 0 + // Preload goes through all existing nodes from the index and add them to the + // status map as unmodified. This is currently the most reliable strategy + // although it comes at a performance cost in large repositories. + // + // This method is recommended when fetching the status of unmodified files. + // For example, to confirm the status of a specific file that is either + // untracked or unmodified. + Preload StatusStrategy = 1 +) + +func (s StatusStrategy) new(w *Worktree) (Status, error) { + switch s { + case Preload: + return preloadStatus(w) + case Empty: + return make(Status), nil + } + return nil, fmt.Errorf("%w: %+v", ErrUnsupportedStatusStrategy, s) +} + +func preloadStatus(w *Worktree) (Status, error) { + idx, err := w.r.Storer.Index() + if err != nil { + return nil, err + } + + idxRoot := mindex.NewRootNode(idx) + nodes := []noder.Noder{idxRoot} + + status := make(Status) + for len(nodes) > 0 { + var node noder.Noder + node, nodes = nodes[0], nodes[1:] + if node.IsDir() { + children, err := node.Children() + if err != nil { + return nil, err + } + nodes = append(nodes, children...) + continue + } + fs := status.File(node.Name()) + fs.Worktree = Unmodified + fs.Staging = Unmodified + } + + return status, nil +} diff --git a/storage/filesystem/dotgit/dotgit.go b/storage/filesystem/dotgit/dotgit.go index ada51eb..72c9ccf 100644 --- a/storage/filesystem/dotgit/dotgit.go +++ b/storage/filesystem/dotgit/dotgit.go @@ -72,6 +72,9 @@ var ( // ErrIsDir is returned when a reference file is attempting to be read, // but the path specified is a directory. ErrIsDir = errors.New("reference path is a directory") + // ErrEmptyRefFile is returned when a reference file is attempted to be read, + // but the file is empty + ErrEmptyRefFile = errors.New("ref file is empty") ) // Options holds configuration for the storage. @@ -661,18 +664,33 @@ func (d *DotGit) readReferenceFrom(rd io.Reader, name string) (ref *plumbing.Ref return nil, err } + if len(b) == 0 { + return nil, ErrEmptyRefFile + } + line := strings.TrimSpace(string(b)) return plumbing.NewReferenceFromStrings(name, line), nil } +// checkReferenceAndTruncate reads the reference from the given file, or the `pack-refs` file if +// the file was empty. Then it checks that the old reference matches the stored reference and +// truncates the file. func (d *DotGit) checkReferenceAndTruncate(f billy.File, old *plumbing.Reference) error { if old == nil { return nil } + ref, err := d.readReferenceFrom(f, old.Name().String()) + if errors.Is(err, ErrEmptyRefFile) { + // This may happen if the reference is being read from a newly created file. + // In that case, try getting the reference from the packed refs file. + ref, err = d.packedRef(old.Name()) + } + if err != nil { return err } + if ref.Hash() != old.Hash() { return storage.ErrReferenceHasChanged } diff --git a/storage/filesystem/dotgit/dotgit_test.go b/storage/filesystem/dotgit/dotgit_test.go index 8a5d8dd..fdb8a57 100644 --- a/storage/filesystem/dotgit/dotgit_test.go +++ b/storage/filesystem/dotgit/dotgit_test.go @@ -16,6 +16,7 @@ import ( "github.com/go-git/go-billy/v5/util" fixtures "github.com/go-git/go-git-fixtures/v4" "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/storage" "github.com/stretchr/testify/assert" . "gopkg.in/check.v1" ) @@ -1046,3 +1047,63 @@ func (s *SuiteDotGit) TestDeletedRefs(c *C) { c.Assert(refs, HasLen, 1) c.Assert(refs[0].Name(), Equals, plumbing.ReferenceName("refs/heads/foo")) } + +// Checks that seting a reference that has been packed and checking its old value is successful +func (s *SuiteDotGit) TestSetPackedRef(c *C) { + fs, clean := s.TemporalFilesystem() + defer clean() + + dir := New(fs) + + err := dir.SetRef(plumbing.NewReferenceFromStrings( + "refs/heads/foo", + "e8d3ffab552895c19b9fcf7aa264d277cde33881", + ), nil) + c.Assert(err, IsNil) + + refs, err := dir.Refs() + c.Assert(err, IsNil) + c.Assert(refs, HasLen, 1) + looseCount, err := dir.CountLooseRefs() + c.Assert(err, IsNil) + c.Assert(looseCount, Equals, 1) + + err = dir.PackRefs() + c.Assert(err, IsNil) + + // Make sure the refs are still there, but no longer loose. + refs, err = dir.Refs() + c.Assert(err, IsNil) + c.Assert(refs, HasLen, 1) + looseCount, err = dir.CountLooseRefs() + c.Assert(err, IsNil) + c.Assert(looseCount, Equals, 0) + + ref, err := dir.Ref("refs/heads/foo") + c.Assert(err, IsNil) + c.Assert(ref, NotNil) + c.Assert(ref.Hash().String(), Equals, "e8d3ffab552895c19b9fcf7aa264d277cde33881") + + // Attempt to update the reference using an invalid old reference value + err = dir.SetRef(plumbing.NewReferenceFromStrings( + "refs/heads/foo", + "b8d3ffab552895c19b9fcf7aa264d277cde33881", + ), plumbing.NewReferenceFromStrings( + "refs/heads/foo", + "e8d3ffab552895c19b9fcf7aa264d277cde33882", + )) + c.Assert(err, Equals, storage.ErrReferenceHasChanged) + + // Now update the reference and it should pass + err = dir.SetRef(plumbing.NewReferenceFromStrings( + "refs/heads/foo", + "b8d3ffab552895c19b9fcf7aa264d277cde33881", + ), plumbing.NewReferenceFromStrings( + "refs/heads/foo", + "e8d3ffab552895c19b9fcf7aa264d277cde33881", + )) + c.Assert(err, IsNil) + looseCount, err = dir.CountLooseRefs() + c.Assert(err, IsNil) + c.Assert(looseCount, Equals, 1) +} diff --git a/storage/filesystem/object.go b/storage/filesystem/object.go index e812fe9..91b4ace 100644 --- a/storage/filesystem/object.go +++ b/storage/filesystem/object.go @@ -431,13 +431,13 @@ func (s *ObjectStorage) getFromUnpacked(h plumbing.Hash) (obj plumbing.EncodedOb defer ioutil.CheckClose(w, &err) - s.objectCache.Put(obj) - bufp := copyBufferPool.Get().(*[]byte) buf := *bufp _, err = io.CopyBuffer(w, r, buf) copyBufferPool.Put(bufp) + s.objectCache.Put(obj) + return obj, err } diff --git a/storage/filesystem/object_test.go b/storage/filesystem/object_test.go index 251077a..4f98458 100644 --- a/storage/filesystem/object_test.go +++ b/storage/filesystem/object_test.go @@ -547,3 +547,64 @@ func BenchmarkGetObjectFromPackfile(b *testing.B) { }) } } + +func (s *FsSuite) TestGetFromUnpackedCachesObjects(c *C) { + fs := fixtures.ByTag(".git").ByTag("unpacked").One().DotGit() + objectCache := cache.NewObjectLRUDefault() + objectStorage := NewObjectStorage(dotgit.New(fs), objectCache) + hash := plumbing.NewHash("f3dfe29d268303fc6e1bbce268605fc99573406e") + + // Assert the cache is empty initially + _, ok := objectCache.Get(hash) + c.Assert(ok, Equals, false) + + // Load the object + obj, err := objectStorage.EncodedObject(plumbing.AnyObject, hash) + c.Assert(err, IsNil) + c.Assert(obj.Hash(), Equals, hash) + + // The object should've been cached during the load + cachedObj, ok := objectCache.Get(hash) + c.Assert(ok, Equals, true) + c.Assert(cachedObj, DeepEquals, obj) + + // Assert that both objects can be read and that they both produce the same bytes + + objReader, err := obj.Reader() + c.Assert(err, IsNil) + objBytes, err := io.ReadAll(objReader) + c.Assert(err, IsNil) + c.Assert(len(objBytes), Not(Equals), 0) + err = objReader.Close() + c.Assert(err, IsNil) + + cachedObjReader, err := cachedObj.Reader() + c.Assert(err, IsNil) + cachedObjBytes, err := io.ReadAll(cachedObjReader) + c.Assert(len(cachedObjBytes), Not(Equals), 0) + c.Assert(err, IsNil) + err = cachedObjReader.Close() + c.Assert(err, IsNil) + + c.Assert(cachedObjBytes, DeepEquals, objBytes) +} + +func (s *FsSuite) TestGetFromUnpackedDoesNotCacheLargeObjects(c *C) { + fs := fixtures.ByTag(".git").ByTag("unpacked").One().DotGit() + objectCache := cache.NewObjectLRUDefault() + objectStorage := NewObjectStorageWithOptions(dotgit.New(fs), objectCache, Options{LargeObjectThreshold: 1}) + hash := plumbing.NewHash("f3dfe29d268303fc6e1bbce268605fc99573406e") + + // Assert the cache is empty initially + _, ok := objectCache.Get(hash) + c.Assert(ok, Equals, false) + + // Load the object + obj, err := objectStorage.EncodedObject(plumbing.AnyObject, hash) + c.Assert(err, IsNil) + c.Assert(obj.Hash(), Equals, hash) + + // The object should not have been cached during the load + _, ok = objectCache.Get(hash) + c.Assert(ok, Equals, false) +} diff --git a/utils/merkletrie/change.go b/utils/merkletrie/change.go index cc6dc89..450feb4 100644 --- a/utils/merkletrie/change.go +++ b/utils/merkletrie/change.go @@ -1,12 +1,17 @@ package merkletrie import ( + "errors" "fmt" "io" "github.com/go-git/go-git/v5/utils/merkletrie/noder" ) +var ( + ErrEmptyFileName = errors.New("empty filename in tree entry") +) + // Action values represent the kind of things a Change can represent: // insertion, deletions or modifications of files. type Action int @@ -121,6 +126,10 @@ func (l *Changes) AddRecursiveDelete(root noder.Path) error { type noderToChangeFn func(noder.Path) Change // NewInsert or NewDelete func (l *Changes) addRecursive(root noder.Path, ctor noderToChangeFn) error { + if root.String() == "" { + return ErrEmptyFileName + } + if !root.IsDir() { l.Add(ctor(root)) return nil diff --git a/utils/merkletrie/change_test.go b/utils/merkletrie/change_test.go index f73eb86..cd28bfe 100644 --- a/utils/merkletrie/change_test.go +++ b/utils/merkletrie/change_test.go @@ -28,6 +28,17 @@ func (s *ChangeSuite) TestUnsupportedAction(c *C) { c.Assert(a.String, PanicMatches, "unsupported action.*") } +func (s ChangeSuite) TestEmptyChanges(c *C) { + ret := merkletrie.NewChanges() + p := noder.Path{} + + err := ret.AddRecursiveInsert(p) + c.Assert(err, Equals, merkletrie.ErrEmptyFileName) + + err = ret.AddRecursiveDelete(p) + c.Assert(err, Equals, merkletrie.ErrEmptyFileName) +} + func (s ChangeSuite) TestNewInsert(c *C) { tree, err := fsnoder.New("(a(b(z<>)))") c.Assert(err, IsNil) diff --git a/worktree_status.go b/worktree_status.go index 10b9d94..6e72db9 100644 --- a/worktree_status.go +++ b/worktree_status.go @@ -29,10 +29,23 @@ var ( // ErrGlobNoMatches in an AddGlob if the glob pattern does not match any // files in the worktree. ErrGlobNoMatches = errors.New("glob pattern did not match any files") + // ErrUnsupportedStatusStrategy occurs when an invalid StatusStrategy is used + // when processing the Worktree status. + ErrUnsupportedStatusStrategy = errors.New("unsupported status strategy") ) // Status returns the working tree status. func (w *Worktree) Status() (Status, error) { + return w.StatusWithOptions(StatusOptions{Strategy: defaultStatusStrategy}) +} + +// StatusOptions defines the options for Worktree.StatusWithOptions(). +type StatusOptions struct { + Strategy StatusStrategy +} + +// StatusWithOptions returns the working tree status. +func (w *Worktree) StatusWithOptions(o StatusOptions) (Status, error) { var hash plumbing.Hash ref, err := w.r.Head() @@ -44,11 +57,14 @@ func (w *Worktree) Status() (Status, error) { hash = ref.Hash() } - return w.status(hash) + return w.status(o.Strategy, hash) } -func (w *Worktree) status(commit plumbing.Hash) (Status, error) { - s := make(Status) +func (w *Worktree) status(ss StatusStrategy, commit plumbing.Hash) (Status, error) { + s, err := ss.new(w) + if err != nil { + return nil, err + } left, err := w.diffCommitWithStaging(commit, false) if err != nil { @@ -543,9 +559,11 @@ func (w *Worktree) doUpdateFileToIndex(e *index.Entry, filename string, h plumbi return err } - if e.Mode.IsRegular() { - e.Size = uint32(info.Size()) - } + // The entry size must always reflect the current state, otherwise + // it will cause go-git's Worktree.Status() to divert from "git status". + // The size of a symlink is the length of the path to the target. + // The size of Regular and Executable files is the size of the files. + e.Size = uint32(info.Size()) fillSystemInfo(e, info.Sys()) return nil diff --git a/worktree_status_test.go b/worktree_status_test.go new file mode 100644 index 0000000..629ebd5 --- /dev/null +++ b/worktree_status_test.go @@ -0,0 +1,89 @@ +package git + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/go-git/go-billy/v5/osfs" + "github.com/go-git/go-git/v5/plumbing/cache" + "github.com/go-git/go-git/v5/storage/filesystem" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// For additional context: #1159. +func TestIndexEntrySizeUpdatedForNonRegularFiles(t *testing.T) { + w := osfs.New(t.TempDir(), osfs.WithBoundOS()) + dot, err := w.Chroot(GitDirName) + require.NoError(t, err) + + s := filesystem.NewStorage(dot, cache.NewObjectLRUDefault()) + r, err := Init(s, w) + require.NoError(t, err) + require.NotNil(t, r) + + wt, err := r.Worktree() + require.NoError(t, err) + require.NotNil(t, wt) + + file := "LICENSE" + f, err := w.OpenFile(file, os.O_CREATE|os.O_WRONLY, 0o666) + require.NoError(t, err) + require.NotNil(t, f) + + content := []byte(strings.Repeat("a\n", 1000)) + _, err = f.Write(content) + require.NoError(t, err) + err = f.Close() + require.NoError(t, err) + + _, err = wt.Add(file) + require.NoError(t, err) + + _, err = wt.Commit("add file", &CommitOptions{}) + require.NoError(t, err) + + st, err := wt.StatusWithOptions(StatusOptions{Strategy: Preload}) + require.NoError(t, err) + assert.Equal(t, + &FileStatus{Worktree: Unmodified, Staging: Unmodified}, + st.File(file)) + + // Make the file not regular. The same would apply to a transition + // from regular file to symlink. + err = os.Chmod(filepath.Join(w.Root(), file), 0o777) + require.NoError(t, err) + + f, err = w.OpenFile(file, os.O_APPEND|os.O_RDWR, 0o777) + require.NoError(t, err) + require.NotNil(t, f) + + _, err = f.Write([]byte("\n\n")) + require.NoError(t, err) + err = f.Close() + require.NoError(t, err) + + _, err = wt.Add(file) + assert.NoError(t, err) + + // go-git's Status diverges from "git status", so this check does not + // fail, even when the issue is present. As at this point "git status" + // reports the unstaged file was modified while "git diff" would return + // empty, as the files are the same but the index has the incorrect file + // size. + st, err = wt.StatusWithOptions(StatusOptions{Strategy: Preload}) + assert.NoError(t, err) + assert.Equal(t, + &FileStatus{Worktree: Unmodified, Staging: Modified}, + st.File(file)) + + idx, err := wt.r.Storer.Index() + assert.NoError(t, err) + require.NotNil(t, idx) + require.Len(t, idx.Entries, 1) + + // Check whether the index was updated with the two new line breaks. + assert.Equal(t, uint32(len(content)+2), idx.Entries[0].Size) +} diff --git a/worktree_test.go b/worktree_test.go index 3e151f6..636ccbe 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -1058,6 +1058,33 @@ func (s *WorktreeSuite) TestStatusEmptyDirty(c *C) { c.Assert(status, HasLen, 1) } +func (s *WorktreeSuite) TestStatusUnmodified(c *C) { + fs := memfs.New() + w := &Worktree{ + r: s.Repository, + Filesystem: fs, + } + + err := w.Checkout(&CheckoutOptions{Force: true}) + c.Assert(err, IsNil) + + status, err := w.StatusWithOptions(StatusOptions{Strategy: Preload}) + c.Assert(err, IsNil) + c.Assert(status.IsClean(), Equals, true) + c.Assert(status.IsUntracked("LICENSE"), Equals, false) + + c.Assert(status.File("LICENSE").Staging, Equals, Unmodified) + c.Assert(status.File("LICENSE").Worktree, Equals, Unmodified) + + status, err = w.StatusWithOptions(StatusOptions{Strategy: Empty}) + c.Assert(err, IsNil) + c.Assert(status.IsClean(), Equals, true) + c.Assert(status.IsUntracked("LICENSE"), Equals, false) + + c.Assert(status.File("LICENSE").Staging, Equals, Untracked) + c.Assert(status.File("LICENSE").Worktree, Equals, Untracked) +} + func (s *WorktreeSuite) TestReset(c *C) { fs := memfs.New() w := &Worktree{ |