diff --git a/commands/command_migrate_import.go b/commands/command_migrate_import.go index e8ce4b63..8b44b415 100644 --- a/commands/command_migrate_import.go +++ b/commands/command_migrate_import.go @@ -196,7 +196,6 @@ func migrateImportCommand(cmd *cobra.Command, args []string) { if err != nil { return err } - return nil } return nil }, @@ -322,8 +321,12 @@ func trackedFromAttrs(db *gitobj.ObjectDatabase, t *gitobj.Tree) (*tools.Ordered for _, e := range t.Entries { if strings.ToLower(e.Name) == ".gitattributes" && e.Type() == gitobj.BlobObjectType { - oid = e.Oid - break + if e.IsLink() { + return nil, errors.Errorf("migrate: %s", tr.Tr.Get("expected '.gitattributes' to be a file, got a symbolic link")) + } else { + oid = e.Oid + break + } } } diff --git a/commands/command_migrate_info.go b/commands/command_migrate_info.go index 47901e6e..bcbc18b2 100644 --- a/commands/command_migrate_info.go +++ b/commands/command_migrate_info.go @@ -164,15 +164,28 @@ func migrateInfoCommand(cmd *cobra.Command, args []string) { }, TreePreCallbackFn: func(path string, t *gitobj.Tree) error { - if migrateFixup && path == "/" { - var err error + if migrateFixup { + if path == "/" { + var err error - fixups, err = gitattr.New(db, t) - if err != nil { - return err + fixups, err = gitattr.New(db, t) + if err != nil { + return err + } } return nil } + + for _, e := range t.Entries { + if strings.ToLower(e.Name) == ".gitattributes" && e.Type() == gitobj.BlobObjectType { + if e.IsLink() { + return errors.Errorf("migrate: %s", tr.Tr.Get("expected '.gitattributes' to be a file, got a symbolic link")) + } else { + break + } + } + } + return nil }, }) diff --git a/docs/man/git-lfs-migrate.1.ronn b/docs/man/git-lfs-migrate.1.ronn index ef481b3c..8ed8cbaf 100644 --- a/docs/man/git-lfs-migrate.1.ronn +++ b/docs/man/git-lfs-migrate.1.ronn @@ -53,7 +53,9 @@ in [INCLUDE AND EXCLUDE]. As typical Git LFS usage depends on tracking specific file types using filename patterns defined in `.gitattributes` files, the `git lfs migrate` command will examine, create, and modify `.gitattributes` files as -necessary. +necessary. The `.gitattributes` files will always be assigned the default +read/write permissions mode (i.e., without execute permissions). Any +symbolic links with that name will cause the migration to halt prematurely. The `import` mode (see [IMPORT]) will convert Git objects of the file types specified (e.g., with `--include`) to Git LFS pointers, and will add entries diff --git a/git/gitattr/tree.go b/git/gitattr/tree.go index d54378fb..d738bd7c 100644 --- a/git/gitattr/tree.go +++ b/git/gitattr/tree.go @@ -3,6 +3,8 @@ package gitattr import ( "strings" + "github.com/git-lfs/git-lfs/v3/errors" + "github.com/git-lfs/git-lfs/v3/tr" "github.com/git-lfs/gitobj/v2" ) @@ -63,6 +65,9 @@ func linesInTree(db *gitobj.ObjectDatabase, t *gitobj.Tree) ([]*Line, string, er var at int = -1 for i, e := range t.Entries { if e.Name == ".gitattributes" { + if e.IsLink() { + return nil, "", errors.Errorf("migrate: %s", tr.Tr.Get("expected '.gitattributes' to be a file, got a symbolic link")) + } at = i break } diff --git a/t/fixtures/migrate.sh b/t/fixtures/migrate.sh index 3fbd26a8..032f0078 100755 --- a/t/fixtures/migrate.sh +++ b/t/fixtures/migrate.sh @@ -28,6 +28,8 @@ assert_ref_unmoved() { # # If "0755" is passed as an argument, the .gitattributes file is created # with that permissions mode. +# If "link" is passed as an argument, the .gitattributes file is created +# as a symlink to a gitattrs file. setup_local_branch_with_gitattrs() { set -e @@ -45,6 +47,12 @@ setup_local_branch_with_gitattrs() { if [[ $1 == "0755" ]]; then chmod +x .gitattributes + elif [[ $1 == "link" ]]; then + mv .gitattributes gitattrs + + add_symlink gitattrs .gitattributes + + git add gitattrs fi git add .gitattributes @@ -128,6 +136,8 @@ setup_single_local_branch_untracked() { # # If "0755" is passed as an argument, the .gitattributes file is created # with that permissions mode. +# If "link" is passed as an argument, the .gitattributes file is created +# as a symlink to a gitattrs file. setup_single_local_branch_tracked() { set -e @@ -150,6 +160,14 @@ setup_single_local_branch_tracked() { git add a.txt a.md git commit -m "add a.{txt,md}" + + if [[ $1 == "link" ]]; then + git mv .gitattributes gitattrs + + add_symlink gitattrs .gitattributes + + git commit -m "link .gitattributes" + fi } # setup_single_local_branch_complex_tracked creates a repository as follows: @@ -191,6 +209,9 @@ setup_single_local_branch_complex_tracked() { # # - Commit 'A' has 120 bytes of random data in a.txt, and tracks *.txt under Git # LFS, but a.txt is not stored as an LFS object. +# +# If "link" is passed as an argument, the .gitattributes file is created +# as a symlink to a gitattrs file. setup_single_local_branch_tracked_corrupt() { set -e @@ -203,6 +224,14 @@ setup_single_local_branch_tracked_corrupt() { base64 < /dev/urandom | head -c 120 > a.txt + if [[ $1 == "link" ]]; then + mv .gitattributes gitattrs + + add_symlink gitattrs .gitattributes + + git add .gitattributes + fi + git add .gitattributes a.txt git commit -m "initial commit" diff --git a/t/t-migrate-export.sh b/t/t-migrate-export.sh index 86aef3b2..69288ce6 100755 --- a/t/t-migrate-export.sh +++ b/t/t-migrate-export.sh @@ -413,6 +413,31 @@ EOF ) end_test +begin_test "migrate export (.gitattributes symlink)" +( + set -e + + setup_single_local_branch_tracked link + + git lfs migrate export --yes --include="*.txt" 2>&1 | tee migrate.log + if [ ${PIPESTATUS[0]} -eq 0 ]; then + echo >&2 "fatal: expected git lfs migrate export to fail, didn't" + exit 1 + fi + + grep "migrate: expected '.gitattributes' to be a file, got a symbolic link" migrate.log + + main="$(git rev-parse refs/heads/main)" + + attrs_main_sha="$(git show $main:.gitattributes | git hash-object --stdin)" + + diff -u <(git ls-tree $main -- .gitattributes) <(cat <<-EOF +120000 blob $attrs_main_sha .gitattributes +EOF + ) +) +end_test + begin_test "migrate export (--object-map)" ( set -e diff --git a/t/t-migrate-fixup.sh b/t/t-migrate-fixup.sh index fff428bc..8f027fce 100755 --- a/t/t-migrate-fixup.sh +++ b/t/t-migrate-fixup.sh @@ -129,3 +129,28 @@ begin_test "migrate import (--fixup with remote tags)" git lfs migrate import --fixup --yes main ) end_test + +begin_test "migrate import (--fixup, .gitattributes symlink)" +( + set -e + + setup_single_local_branch_tracked_corrupt link + + git lfs migrate import --everything --fixup --yes 2>&1 | tee migrate.log + if [ ${PIPESTATUS[0]} -eq 0 ]; then + echo >&2 "fatal: expected git lfs migrate import to fail, didn't" + exit 1 + fi + + grep "migrate: expected '.gitattributes' to be a file, got a symbolic link" migrate.log + + main="$(git rev-parse refs/heads/main)" + + attrs_main_sha="$(git show $main:.gitattributes | git hash-object --stdin)" + + diff -u <(git ls-tree $main -- .gitattributes) <(cat <<-EOF +120000 blob $attrs_main_sha .gitattributes +EOF + ) +) +end_test diff --git a/t/t-migrate-import.sh b/t/t-migrate-import.sh index 54c5d64d..ab56877f 100755 --- a/t/t-migrate-import.sh +++ b/t/t-migrate-import.sh @@ -606,6 +606,31 @@ EOF ) end_test +begin_test "migrate import (existing .gitattributes symlink)" +( + set -e + + setup_local_branch_with_gitattrs link + + git lfs migrate import --yes --include-ref=refs/heads/main --include="*.txt" 2>&1 | tee migrate.log + if [ ${PIPESTATUS[0]} -eq 0 ]; then + echo >&2 "fatal: expected git lfs migrate import to fail, didn't" + exit 1 + fi + + grep "migrate: expected '.gitattributes' to be a file, got a symbolic link" migrate.log + + main="$(git rev-parse refs/heads/main)" + + attrs_main_sha="$(git show $main:.gitattributes | git hash-object --stdin)" + + diff -u <(git ls-tree $main -- .gitattributes) <(cat <<-EOF +120000 blob $attrs_main_sha .gitattributes +EOF + ) +) +end_test + begin_test "migrate import (identical contents, different permissions)" ( set -e diff --git a/t/t-migrate-info.sh b/t/t-migrate-info.sh index f0ecb494..7c6d9b6b 100755 --- a/t/t-migrate-info.sh +++ b/t/t-migrate-info.sh @@ -432,6 +432,56 @@ begin_test "migrate info (--everything)" ) end_test +begin_test "migrate info (existing .gitattributes symlink)" +( + set -e + + setup_local_branch_with_gitattrs link + + git lfs migrate info --everything 2>&1 | tee migrate.log + if [ ${PIPESTATUS[0]} -eq 0 ]; then + echo >&2 "fatal: expected git lfs migrate info to fail, didn't" + exit 1 + fi + + grep "migrate: expected '.gitattributes' to be a file, got a symbolic link" migrate.log + + main="$(git rev-parse refs/heads/main)" + + attrs_main_sha="$(git show $main:.gitattributes | git hash-object --stdin)" + + diff -u <(git ls-tree $main -- .gitattributes) <(cat <<-EOF +120000 blob $attrs_main_sha .gitattributes +EOF + ) +) +end_test + +begin_test "migrate info (potential fixup, --fixup, .gitattributes symlink)" +( + set -e + + setup_single_local_branch_tracked_corrupt link + + git lfs migrate info 2>&1 | tee migrate.log + if [ ${PIPESTATUS[0]} -eq 0 ]; then + echo >&2 "fatal: expected git lfs migrate info to fail, didn't" + exit 1 + fi + + grep "migrate: expected '.gitattributes' to be a file, got a symbolic link" migrate.log + + main="$(git rev-parse refs/heads/main)" + + attrs_main_sha="$(git show $main:.gitattributes | git hash-object --stdin)" + + diff -u <(git ls-tree $main -- .gitattributes) <(cat <<-EOF +120000 blob $attrs_main_sha .gitattributes +EOF + ) +) +end_test + begin_test "migrate info (--fixup, no .gitattributes)" ( set -e