2015-05-04 05:36:34 +00:00
|
|
|
package commands
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/hex"
|
2021-06-09 20:45:03 +00:00
|
|
|
"fmt"
|
2015-05-04 05:36:34 +00:00
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2021-06-10 18:02:43 +00:00
|
|
|
"strings"
|
2015-05-04 05:36:34 +00:00
|
|
|
|
2021-06-09 20:45:03 +00:00
|
|
|
"github.com/git-lfs/git-lfs/errors"
|
2018-02-01 02:16:43 +00:00
|
|
|
"github.com/git-lfs/git-lfs/filepathfilter"
|
2016-11-15 17:01:18 +00:00
|
|
|
"github.com/git-lfs/git-lfs/git"
|
|
|
|
"github.com/git-lfs/git-lfs/lfs"
|
2018-12-05 16:15:52 +00:00
|
|
|
"github.com/git-lfs/git-lfs/tools"
|
2016-05-23 18:02:27 +00:00
|
|
|
"github.com/spf13/cobra"
|
2015-05-04 05:36:34 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2021-06-09 20:45:03 +00:00
|
|
|
fsckDryRun bool
|
|
|
|
fsckObjects bool
|
|
|
|
fsckPointers bool
|
2015-05-04 05:36:34 +00:00
|
|
|
)
|
|
|
|
|
2021-06-09 20:45:03 +00:00
|
|
|
type corruptPointer struct {
|
|
|
|
blobOid string
|
|
|
|
treeOid string
|
|
|
|
lfsOid string
|
|
|
|
path string
|
|
|
|
message string
|
|
|
|
kind string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p corruptPointer) String() string {
|
|
|
|
return fmt.Sprintf("%s: %s", p.kind, p.message)
|
|
|
|
}
|
|
|
|
|
2016-11-29 20:56:47 +00:00
|
|
|
// TODO(zeroshirts): 'git fsck' reports status (percentage, current#/total) as
|
|
|
|
// it checks... we should do the same, as we are rehashing potentially gigs and
|
|
|
|
// gigs of content.
|
|
|
|
//
|
|
|
|
// NOTE(zeroshirts): Ideally git would have hooks for fsck such that we could
|
|
|
|
// chain a lfs-fsck, but I don't think it does.
|
|
|
|
func fsckCommand(cmd *cobra.Command, args []string) {
|
2017-10-18 21:42:00 +00:00
|
|
|
installHooks(false)
|
commands: make sure we're in the working tree
In the normal case, Git commands perform repository autodiscovery based
on the current working directory. However, in some cases, it's possible
to specify a Git working tree unrelated to the current working directory
by using GIT_WORK_TREE. In such a case, we want to make sure that we
change into the working tree such that our working directory is always
within the working tree, if one exists. This is what Git does, and it
means that when we write files into the repository, such as a
.gitattributes file, we write them into the proper place.
Note also that we adjust the code to require that the working directory
be non-empty when we require a working copy instead of that the
repository be non-bare. That's because we don't want people to be
working inside of the Git directory in such situations, where the
repository would be non-bare but would not have a working tree.
We add tests for this case for track and untrack, which require a
working tree, and for checkout, which requires only a repository. This
means that we can verify the behavior of the functions we've added
without needing to add tests for this case to each of the subcommands.
2020-10-02 19:03:55 +00:00
|
|
|
setupRepository()
|
2015-09-08 15:29:53 +00:00
|
|
|
|
2021-06-10 18:02:43 +00:00
|
|
|
useIndex := false
|
|
|
|
start := ""
|
|
|
|
end := "HEAD"
|
|
|
|
|
|
|
|
switch len(args) {
|
|
|
|
case 0:
|
|
|
|
useIndex = true
|
|
|
|
ref, err := git.CurrentRef()
|
|
|
|
if err != nil {
|
|
|
|
ExitWithError(err)
|
|
|
|
}
|
|
|
|
end = ref.Sha
|
|
|
|
case 1:
|
|
|
|
pieces := strings.SplitN(args[0], "..", 2)
|
|
|
|
refs, err := git.ResolveRefs(pieces)
|
|
|
|
if err != nil {
|
|
|
|
ExitWithError(err)
|
|
|
|
}
|
|
|
|
if len(refs) == 2 {
|
|
|
|
start = refs[0].Sha
|
|
|
|
end = refs[1].Sha
|
|
|
|
} else {
|
|
|
|
end = refs[0].Sha
|
|
|
|
}
|
2015-05-04 05:36:34 +00:00
|
|
|
}
|
|
|
|
|
2021-06-09 20:45:03 +00:00
|
|
|
if !fsckPointers && !fsckObjects {
|
|
|
|
fsckPointers = true
|
|
|
|
fsckObjects = true
|
|
|
|
}
|
|
|
|
|
2021-03-23 18:22:33 +00:00
|
|
|
ok := true
|
|
|
|
var corruptOids []string
|
2021-06-09 20:45:03 +00:00
|
|
|
var corruptPointers []corruptPointer
|
|
|
|
if fsckObjects {
|
|
|
|
corruptOids = doFsckObjects(start, end, useIndex)
|
|
|
|
ok = ok && len(corruptOids) == 0
|
|
|
|
}
|
|
|
|
if fsckPointers {
|
|
|
|
corruptPointers = doFsckPointers(start, end)
|
|
|
|
ok = ok && len(corruptPointers) == 0
|
|
|
|
}
|
2021-03-23 18:22:33 +00:00
|
|
|
|
|
|
|
if ok {
|
|
|
|
Print("Git LFS fsck OK")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-06-10 16:21:34 +00:00
|
|
|
if fsckDryRun || len(corruptOids) == 0 {
|
|
|
|
os.Exit(1)
|
2021-03-23 18:22:33 +00:00
|
|
|
}
|
|
|
|
|
2021-06-10 16:21:34 +00:00
|
|
|
badDir := filepath.Join(cfg.LFSStorageDir(), "bad")
|
|
|
|
Print("objects: repair: moving corrupt objects to %s", badDir)
|
2021-03-23 18:22:33 +00:00
|
|
|
|
2021-06-10 16:21:34 +00:00
|
|
|
if err := tools.MkdirAll(badDir, cfg); err != nil {
|
|
|
|
ExitWithError(err)
|
|
|
|
}
|
2021-03-23 18:22:33 +00:00
|
|
|
|
2021-06-10 16:21:34 +00:00
|
|
|
for _, oid := range corruptOids {
|
|
|
|
badFile := filepath.Join(badDir, oid)
|
|
|
|
if err := os.Rename(cfg.Filesystem().ObjectPathname(oid), badFile); err != nil {
|
|
|
|
ExitWithError(err)
|
2021-03-23 18:22:33 +00:00
|
|
|
}
|
|
|
|
}
|
2021-06-10 16:21:34 +00:00
|
|
|
os.Exit(1)
|
2021-03-23 18:22:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// doFsckObjects checks that the objects in the given ref are correct and exist.
|
2021-06-10 18:02:43 +00:00
|
|
|
func doFsckObjects(start, end string, useIndex bool) []string {
|
2016-11-29 20:56:47 +00:00
|
|
|
var corruptOids []string
|
2019-08-09 15:25:23 +00:00
|
|
|
gitscanner := lfs.NewGitScanner(cfg, func(p *lfs.WrappedPointer, err error) {
|
2016-11-29 20:56:47 +00:00
|
|
|
if err == nil {
|
|
|
|
var pointerOk bool
|
2021-06-09 19:45:27 +00:00
|
|
|
pointerOk, err = fsckPointer(p.Name, p.Oid, p.Size)
|
2016-11-29 20:56:47 +00:00
|
|
|
if !pointerOk {
|
|
|
|
corruptOids = append(corruptOids, p.Oid)
|
|
|
|
}
|
|
|
|
}
|
2015-05-04 05:36:34 +00:00
|
|
|
|
2016-11-29 20:56:47 +00:00
|
|
|
if err != nil {
|
|
|
|
Panic(err, "Error checking Git LFS files")
|
|
|
|
}
|
|
|
|
})
|
2015-05-08 16:20:02 +00:00
|
|
|
|
2018-02-01 02:16:43 +00:00
|
|
|
// If 'lfs.fetchexclude' is set and 'git lfs fsck' is run after the
|
|
|
|
// initial fetch (i.e., has elected to fetch a subset of Git LFS
|
|
|
|
// objects), the "missing" ones will fail the fsck.
|
|
|
|
//
|
|
|
|
// Attach a filepathfilter to avoid _only_ the excluded paths.
|
|
|
|
gitscanner.Filter = filepathfilter.New(nil, cfg.FetchExcludePaths())
|
|
|
|
|
2021-06-10 18:02:43 +00:00
|
|
|
if start == "" {
|
|
|
|
if err := gitscanner.ScanRef(end, nil); err != nil {
|
|
|
|
ExitWithError(err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if err := gitscanner.ScanRefRange(start, end, nil); err != nil {
|
|
|
|
ExitWithError(err)
|
|
|
|
}
|
2016-11-29 20:56:47 +00:00
|
|
|
}
|
2015-05-04 05:36:34 +00:00
|
|
|
|
2021-06-10 18:02:43 +00:00
|
|
|
if useIndex {
|
|
|
|
if err := gitscanner.ScanIndex("HEAD", nil); err != nil {
|
|
|
|
ExitWithError(err)
|
|
|
|
}
|
2016-11-29 20:56:47 +00:00
|
|
|
}
|
2015-05-04 05:36:34 +00:00
|
|
|
|
2016-11-29 20:56:47 +00:00
|
|
|
gitscanner.Close()
|
2021-03-23 18:22:33 +00:00
|
|
|
return corruptOids
|
2015-05-04 05:36:34 +00:00
|
|
|
}
|
|
|
|
|
2021-06-09 20:45:03 +00:00
|
|
|
// doFsckPointers checks that the pointers in the given ref are correct and canonical.
|
|
|
|
func doFsckPointers(start, end string) []corruptPointer {
|
|
|
|
var corruptPointers []corruptPointer
|
|
|
|
gitscanner := lfs.NewGitScanner(cfg, func(p *lfs.WrappedPointer, err error) {
|
|
|
|
if p != nil {
|
|
|
|
Debug("Examining %v (%v)", p.Oid, p.Name)
|
|
|
|
if !p.Canonical {
|
|
|
|
cp := corruptPointer{
|
|
|
|
blobOid: p.Sha1,
|
|
|
|
lfsOid: p.Oid,
|
|
|
|
message: fmt.Sprintf("Pointer for %s (blob %s) was not canonical", p.Oid, p.Sha1),
|
|
|
|
kind: "nonCanonicalPointer",
|
|
|
|
}
|
|
|
|
Print("pointer: %s", cp.String())
|
|
|
|
corruptPointers = append(corruptPointers, cp)
|
|
|
|
}
|
|
|
|
} else if errors.IsPointerScanError(err) {
|
|
|
|
psErr, ok := err.(errors.PointerScanError)
|
|
|
|
if ok {
|
|
|
|
cp := corruptPointer{
|
|
|
|
treeOid: psErr.OID(),
|
|
|
|
path: psErr.Path(),
|
|
|
|
message: fmt.Sprintf("%q (treeish %s) should have been a pointer but was not", psErr.Path(), psErr.OID()),
|
|
|
|
kind: "unexpectedGitObject",
|
|
|
|
}
|
|
|
|
Print("pointer: %s", cp.String())
|
|
|
|
corruptPointers = append(corruptPointers, cp)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Panic(err, "Error checking Git LFS files")
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2021-06-10 18:02:43 +00:00
|
|
|
if start == "" {
|
|
|
|
if err := gitscanner.ScanRefByTree(end, nil); err != nil {
|
|
|
|
ExitWithError(err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if err := gitscanner.ScanRefRangeByTree(start, end, nil); err != nil {
|
|
|
|
ExitWithError(err)
|
|
|
|
}
|
2021-06-09 20:45:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
gitscanner.Close()
|
|
|
|
return corruptPointers
|
|
|
|
}
|
|
|
|
|
2021-06-09 19:45:27 +00:00
|
|
|
func fsckPointer(name, oid string, size int64) (bool, error) {
|
2017-10-25 17:33:04 +00:00
|
|
|
path := cfg.Filesystem().ObjectPathname(oid)
|
2016-11-29 17:02:21 +00:00
|
|
|
|
2016-11-29 20:56:47 +00:00
|
|
|
Debug("Examining %v (%v)", name, path)
|
2016-11-29 17:02:21 +00:00
|
|
|
|
2016-11-29 20:56:47 +00:00
|
|
|
f, err := os.Open(path)
|
|
|
|
if pErr, pOk := err.(*os.PathError); pOk {
|
2021-06-09 19:45:27 +00:00
|
|
|
// This is an empty file. No problem here.
|
|
|
|
if size == 0 {
|
|
|
|
return true, nil
|
|
|
|
}
|
2021-04-19 19:50:06 +00:00
|
|
|
Print("objects: openError: %s (%s) could not be checked: %s", name, oid, pErr.Err)
|
2016-11-29 20:56:47 +00:00
|
|
|
return false, nil
|
2016-11-29 17:02:21 +00:00
|
|
|
}
|
|
|
|
|
2016-11-29 20:56:47 +00:00
|
|
|
if err != nil {
|
|
|
|
return false, err
|
2016-11-29 17:02:21 +00:00
|
|
|
}
|
|
|
|
|
2016-11-29 20:56:47 +00:00
|
|
|
oidHash := sha256.New()
|
|
|
|
_, err = io.Copy(oidHash, f)
|
|
|
|
f.Close()
|
2015-05-04 05:36:34 +00:00
|
|
|
if err != nil {
|
2016-11-29 20:56:47 +00:00
|
|
|
return false, err
|
2015-05-08 16:20:02 +00:00
|
|
|
}
|
|
|
|
|
2016-11-29 20:56:47 +00:00
|
|
|
recalculatedOid := hex.EncodeToString(oidHash.Sum(nil))
|
|
|
|
if recalculatedOid == oid {
|
|
|
|
return true, nil
|
2015-05-04 05:36:34 +00:00
|
|
|
}
|
2016-11-29 20:56:47 +00:00
|
|
|
|
2021-04-19 19:50:06 +00:00
|
|
|
Print("objects: corruptObject: %s (%s) is corrupt", name, oid)
|
2016-11-29 20:56:47 +00:00
|
|
|
return false, nil
|
2015-05-04 05:36:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
2016-09-01 16:09:38 +00:00
|
|
|
RegisterCommand("fsck", fsckCommand, func(cmd *cobra.Command) {
|
2016-08-10 15:33:25 +00:00
|
|
|
cmd.Flags().BoolVarP(&fsckDryRun, "dry-run", "d", false, "List corrupt objects without deleting them.")
|
2021-06-09 20:45:03 +00:00
|
|
|
cmd.Flags().BoolVarP(&fsckObjects, "objects", "", false, "Fsck objects.")
|
|
|
|
cmd.Flags().BoolVarP(&fsckPointers, "pointers", "", false, "Fsck pointers.")
|
2016-08-10 15:33:25 +00:00
|
|
|
})
|
2015-05-04 05:36:34 +00:00
|
|
|
}
|