├── .ruby-version ├── debian ├── compat ├── source │ └── format ├── git-lfs.manpages ├── postinst ├── prerm ├── git-lfs.lintian-overrides ├── control └── copyright ├── script ├── distro-tool ├── windows-installer │ ├── git-lfs-logo.bmp │ ├── git-lfs-logo.ico │ └── git-lfs-wizard-image.bmp ├── compile-win-installer-unsigned.bat ├── install-git-source ├── macos │ └── manifest.json ├── notarize ├── install.sh ├── cibuild └── hash-files ├── t ├── fixtures │ └── templates │ │ ├── info │ │ └── exclude │ │ └── HEAD ├── git-lfs-test-server-api │ └── .gitignore ├── cmd │ ├── git-credential-lfsnoop.go │ ├── lfs-ssh-proxy-test.go │ ├── lfstest-badpathcheck.go │ ├── lfstest-nanomtime.go │ ├── lfs-askpass.go │ ├── lfstest-realpath.go │ └── lfstest-genrandom.go ├── t-push-file-with-branch-name.sh ├── t-logs.sh ├── t-version.sh ├── t-object-authenticated.sh ├── t-clone-deprecated.sh ├── t-unusual-filenames.sh ├── t-progress-meter.sh ├── t-uninstall-worktree-unsupported.sh ├── t-install-worktree-unsupported.sh ├── t-install-custom-hooks-path-unsupported.sh ├── t-repo-format.sh ├── t-upload-redirect.sh ├── t-batch-unknown-oids.sh ├── t-cherry-pick-commits.sh ├── t-push-bad-dns.sh ├── t-tempfile.sh ├── t-completion.sh ├── t-filter-branch.sh ├── t-mergetool.sh ├── t-track-attrs.sh ├── t-credentials-no-prompt.sh ├── t-progress.sh ├── t-duplicate-oids.sh ├── t-ext.sh ├── t-commit-delete-push.sh ├── t-submodule-recurse.sh ├── t-expired.sh ├── t-batch-error-handling.sh ├── t-zero-len-file.sh └── t-ssh.sh ├── .github ├── CODEOWNERS ├── dependabot.yml └── ISSUE_TEMPLATE │ ├── other-issue.md │ └── bug_report.md ├── .gitattributes ├── git ├── githistory │ └── fixtures │ │ ├── octopus-merge.git │ │ ├── HEAD │ │ ├── ORIG_HEAD │ │ ├── refs │ │ │ └── heads │ │ │ │ ├── branch-a │ │ │ │ ├── branch-b │ │ │ │ └── master │ │ ├── index │ │ ├── config │ │ ├── objects │ │ │ ├── 15 │ │ │ │ └── 805fe2044dc1a0508853e93d1a230bd94636be │ │ │ ├── 25 │ │ │ │ └── 1e6b3461a3b5adc6bab694d5ae1abc878edf85 │ │ │ ├── 63 │ │ │ │ └── d8dbd40c23542e740659a7168a0ce3138ea748 │ │ │ ├── 04 │ │ │ │ └── df07b08ca746b3167d0f1d1514e2f39a52c16c │ │ │ ├── 2e │ │ │ │ └── 65efe2a145dda7ee51d1741299f848e5bf752e │ │ │ ├── 2f │ │ │ │ └── 3a8ec99d5b459b41b6675e52758e397c5e3103 │ │ │ ├── 5a │ │ │ │ └── 0581b158a81b8fc5d2169c2ac2ca7772ff13fd │ │ │ ├── 6c │ │ │ │ └── 9ccaeb45446e3fa88cd5848a940fd34c18192b │ │ │ ├── 8b │ │ │ │ └── e6d64cddab01f53381e9feafe50d95ca5e6629 │ │ │ ├── b6 │ │ │ │ └── fc4c620b67d95f953a5c1c1230aaab5db5a1b0 │ │ │ └── e9 │ │ │ │ └── 4edfabfb7605f7cb959b4ce8fb6652b509fe03 │ │ └── logs │ │ │ ├── refs │ │ │ └── heads │ │ │ │ ├── branch-a │ │ │ │ ├── branch-b │ │ │ │ └── master │ │ │ └── HEAD │ │ ├── identical-blobs.git │ │ ├── HEAD │ │ ├── refs │ │ │ └── heads │ │ │ │ └── master │ │ ├── index │ │ ├── config │ │ ├── logs │ │ │ ├── HEAD │ │ │ └── refs │ │ │ │ └── heads │ │ │ │ └── master │ │ └── objects │ │ │ ├── 42 │ │ │ └── 723ad796caa500ddf4e3f6ad37600ed5a65491 │ │ │ ├── 94 │ │ │ └── f3610c08588440112ed977376f26a8fba169b0 │ │ │ └── b6 │ │ │ └── 2b45ed2b59cf32dd676ca47497e76a1dab9c7e │ │ ├── linear-history.git │ │ ├── HEAD │ │ ├── refs │ │ │ └── heads │ │ │ │ └── master │ │ ├── index │ │ ├── config │ │ ├── objects │ │ │ ├── 56 │ │ │ │ └── a6051ca2b02b04ef92d5150c9ef600403cb1de │ │ │ ├── 62 │ │ │ │ └── 811b8f930323895033b3b338c35f51c0b7268b │ │ │ ├── 71 │ │ │ │ └── a488ec1804ee97ea651b094aa9181ca85aab0a │ │ │ ├── 6e │ │ │ │ └── 07bd31cb70c4add2c973481ad4fa38b235ca69 │ │ │ ├── c5 │ │ │ │ └── decfe1fcf39b8c489f4a0bf3b3823676339f80 │ │ │ ├── d8 │ │ │ │ └── 263ee9860594d2806b0dfd1bfd17528b0ba2a4 │ │ │ ├── e4 │ │ │ │ └── 40e5c842586965a7fb77deda2eca68612b1f53 │ │ │ ├── e6 │ │ │ │ └── 69b63f829bfb0b91fc52a5bcea53dd7977a0ee │ │ │ └── ef │ │ │ │ └── eab7a9b61312fa56fc74eee1e0f5a714abfb70 │ │ └── logs │ │ │ ├── HEAD │ │ │ └── refs │ │ │ └── heads │ │ │ └── master │ │ ├── packed-objects.git │ │ ├── HEAD │ │ ├── refs │ │ │ └── heads │ │ │ │ └── master │ │ ├── info │ │ │ └── refs │ │ ├── objects │ │ │ ├── info │ │ │ │ └── packs │ │ │ └── pack │ │ │ │ ├── pack-ac516ce2d006668dc5e001e8dda0aa1c7198500f.idx │ │ │ │ └── pack-ac516ce2d006668dc5e001e8dda0aa1c7198500f.pack │ │ ├── index │ │ ├── config │ │ └── logs │ │ │ ├── HEAD │ │ │ └── refs │ │ │ └── heads │ │ │ └── master │ │ ├── repeated-subtrees.git │ │ ├── HEAD │ │ ├── refs │ │ │ └── heads │ │ │ │ └── master │ │ ├── index │ │ ├── config │ │ ├── objects │ │ │ ├── 12 │ │ │ │ └── b98c239e8f933d213617a1b965333d478b2743 │ │ │ ├── 47 │ │ │ │ └── d4d71022adc7ec6a14250d23491e535ec228f4 │ │ │ ├── 63 │ │ │ │ └── d8dbd40c23542e740659a7168a0ce3138ea748 │ │ │ ├── 0b │ │ │ │ └── 4747509ab885114690ff291f8f108045b1d749 │ │ │ ├── 2e │ │ │ │ └── 65efe2a145dda7ee51d1741299f848e5bf752e │ │ │ ├── 5e │ │ │ │ └── 497ceceb14ad3c43bac781ed5c804bc67e8f3b │ │ │ ├── b9 │ │ │ │ └── 621d5d84b3174de020ad2c869f43b2f61f337f │ │ │ └── bc │ │ │ │ └── 4d1181aca5a33673d7c5d4c209d09ce1cfabd7 │ │ └── logs │ │ │ ├── HEAD │ │ │ └── refs │ │ │ └── heads │ │ │ └── master │ │ ├── non-repeated-subtrees.git │ │ ├── HEAD │ │ ├── refs │ │ │ └── heads │ │ │ │ └── master │ │ ├── index │ │ ├── config │ │ ├── objects │ │ │ ├── 12 │ │ │ │ └── 7ececad475cde6da0048051d62121cabd23194 │ │ │ ├── 19 │ │ │ │ └── acdd81ab0abc15c771fe005bf1c2825e4e6080 │ │ │ ├── 37 │ │ │ │ └── f99c7f2706d317b3bf7ff13d574eef33d8788a │ │ │ ├── 07 │ │ │ │ └── bd7fbfc41b7d36135bcffe7c465490f4aca32d │ │ │ ├── 3d │ │ │ │ └── 1baaaceec085c52e3e57a47a75b87b7615d0ef │ │ │ ├── 8d │ │ │ │ └── 14cbf983b3fad683171c9418998d9f68340823 │ │ │ └── bc │ │ │ │ └── 63077ac5e575ccc9dbbd93dc882f1e10600ea7 │ │ └── logs │ │ │ ├── HEAD │ │ │ └── refs │ │ │ └── heads │ │ │ └── master │ │ ├── linear-history-with-tags.git │ │ ├── HEAD │ │ ├── refs │ │ │ ├── heads │ │ │ │ └── master │ │ │ └── tags │ │ │ │ └── middle │ │ ├── index │ │ ├── config │ │ ├── objects │ │ │ ├── 20 │ │ │ │ └── ecedad3e74a113695fe5f00ab003694e2e1e9c │ │ │ ├── 22 │ │ │ │ └── 8afe30855933151f7a88e70d9d88314fd2f191 │ │ │ ├── 34 │ │ │ │ └── 10062ba67c5ed59b854387a8bc0ec012479368 │ │ │ ├── 52 │ │ │ │ └── a8963f48d54c7d352695a278ca4b025e130cb4 │ │ │ ├── 63 │ │ │ │ └── d8dbd40c23542e740659a7168a0ce3138ea748 │ │ │ ├── 91 │ │ │ │ └── b85be6928569390e937479509b80a1d0dccb0c │ │ │ ├── 2e │ │ │ │ └── 65efe2a145dda7ee51d1741299f848e5bf752e │ │ │ ├── 3c │ │ │ │ └── b3201d7942353fff5f45e03d114e8e7a061f87 │ │ │ └── d9 │ │ │ │ └── 41e4756add6b06f5bee766fcf669f55419f13f │ │ └── logs │ │ │ ├── HEAD │ │ │ └── refs │ │ │ └── heads │ │ │ └── master │ │ └── linear-history-with-annotated-tags.git │ │ ├── HEAD │ │ ├── refs │ │ ├── heads │ │ │ └── master │ │ └── tags │ │ │ └── middle │ │ ├── index │ │ ├── config │ │ ├── objects │ │ ├── 20 │ │ │ └── ecedad3e74a113695fe5f00ab003694e2e1e9c │ │ ├── 22 │ │ │ └── 8afe30855933151f7a88e70d9d88314fd2f191 │ │ ├── 34 │ │ │ └── 10062ba67c5ed59b854387a8bc0ec012479368 │ │ ├── 52 │ │ │ └── a8963f48d54c7d352695a278ca4b025e130cb4 │ │ ├── 63 │ │ │ └── d8dbd40c23542e740659a7168a0ce3138ea748 │ │ ├── 91 │ │ │ └── b85be6928569390e937479509b80a1d0dccb0c │ │ ├── 05 │ │ │ └── 797a38b05f910e6efe40dc1a5c0a046a9403e8 │ │ ├── 2e │ │ │ └── 65efe2a145dda7ee51d1741299f848e5bf752e │ │ ├── 3c │ │ │ └── b3201d7942353fff5f45e03d114e8e7a061f87 │ │ ├── 4a │ │ │ └── 78e180c45f18489941174df19d538c26d5318b │ │ ├── 6b │ │ │ └── de0b381fa1a039396445e2ce5a28c0451fde15 │ │ └── d9 │ │ │ └── 41e4756add6b06f5bee766fcf669f55419f13f │ │ └── logs │ │ ├── HEAD │ │ └── refs │ │ └── heads │ │ └── master ├── config_test.go ├── filter_process_status.go └── ls_tree_scanner_test.go ├── docs ├── api │ ├── schemas │ │ ├── http-batch-request-schema.json │ │ ├── http-batch-response-schema.json │ │ ├── http-lock-list-response-schema.json │ │ ├── http-lock-create-request-schema.json │ │ ├── http-lock-create-response-schema.json │ │ ├── http-lock-delete-request-schema.json │ │ └── http-lock-verify-response-schema.json │ └── README.md ├── man │ ├── git-lfs-env.adoc │ ├── git-lfs-post-merge.adoc │ ├── git-lfs-post-checkout.adoc │ ├── git-lfs-untrack.adoc │ ├── git-lfs-ext.adoc │ ├── git-lfs-clean.adoc │ ├── git-lfs-logs.adoc │ ├── git-lfs-standalone-file.adoc │ ├── git-lfs-post-commit.adoc │ ├── git-lfs-update.adoc │ ├── git-lfs-dedup.adoc │ ├── git-lfs-unlock.adoc │ ├── git-lfs-lock.adoc │ ├── git-lfs-pre-push.adoc │ ├── git-lfs-status.adoc │ ├── git-lfs-fsck.adoc │ ├── git-lfs-pointer.adoc │ ├── git-lfs-push.adoc │ ├── git-lfs-uninstall.adoc │ └── git-lfs-filter-process.adoc ├── proposals │ ├── README.md │ └── ntlm.md └── README.md ├── git-lfs_windows.go ├── tools ├── cygwin.go ├── umask_windows.go ├── math.go ├── humanize │ └── package.go ├── umask_nix.go ├── robustio.go ├── filetools_nix.go ├── math_test.go ├── util_generic.go ├── time_tools.go ├── robustio_windows.go ├── util_test.go ├── channels.go ├── cygwin_windows.go ├── filetools_windows.go ├── sync_writer.go ├── util_darwin_test.go ├── util_linux.go └── os_tools.go ├── git-lfs_windows_arm64.go ├── config ├── util_windows.go ├── util_nix.go ├── version.go ├── fetcher.go ├── extension.go ├── map_fetcher.go ├── git_fetcher_test.go └── extension_test.go ├── commands ├── path_nix.go ├── path.go ├── pointers.go ├── command_standalone_file.go ├── multiwriter.go ├── command_version.go ├── command_ext.go ├── uploader_test.go ├── path_windows.go ├── command_post_merge.go ├── commands_test.go ├── command_post_commit.go └── command_untrack.go ├── subprocess ├── buffered_cmd.go ├── subprocess_nix.go ├── subprocess_windows.go └── cmd.go ├── po └── es.po ├── versioninfo.json ├── locking ├── schemas │ ├── http-lock-delete-request-schema.json │ ├── http-lock-create-request-schema.json │ ├── http-lock-list-response-schema.json │ ├── http-lock-create-response-schema.json │ └── http-lock-verify-response-schema.json └── cache_test.go ├── lfsapi ├── kerberos.go ├── body.go └── client.go ├── .gitignore ├── tq ├── errors_test.go ├── errors.go ├── schemas │ └── http-batch-request-schema.json ├── manifest_test.go └── verify.go ├── lfshttp ├── cookies.go ├── body.go └── retries.go ├── git-lfs.go ├── lfs ├── gitfilter.go ├── config_test.go ├── gitscanner_remotes.go └── gitscanner_catfilebatchcheckscanner_test.go ├── creds └── access.go ├── errors ├── types_test.go ├── context.go └── errors_test.go ├── tasklog ├── waiting_task.go ├── task.go ├── list_task.go ├── list_task_test.go ├── waiting_task_test.go └── simple_task_test.go ├── fs └── fs_test.go ├── .mailmap └── tr └── tr.go /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.2 2 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /script/distro-tool: -------------------------------------------------------------------------------- 1 | lib/distro.rb -------------------------------------------------------------------------------- /t/fixtures/templates/info/exclude: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @git-lfs/core 2 | -------------------------------------------------------------------------------- /t/fixtures/templates/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/main 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | * eol=lf 3 | *.bat eol=crlf 4 | -------------------------------------------------------------------------------- /t/git-lfs-test-server-api/.gitignore: -------------------------------------------------------------------------------- 1 | git-lfs-test-server-api* 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /debian/git-lfs.manpages: -------------------------------------------------------------------------------- 1 | man/man1/*.1 2 | man/man5/*.5 3 | man/man7/*.7 4 | -------------------------------------------------------------------------------- /git/githistory/fixtures/identical-blobs.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/packed-objects.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/repeated-subtrees.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/non-repeated-subtrees.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /docs/api/schemas/http-batch-request-schema.json: -------------------------------------------------------------------------------- 1 | ../../../tq/schemas/http-batch-request-schema.json -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/ORIG_HEAD: -------------------------------------------------------------------------------- 1 | 8be6d64cddab01f53381e9feafe50d95ca5e6629 2 | -------------------------------------------------------------------------------- /docs/api/schemas/http-batch-response-schema.json: -------------------------------------------------------------------------------- 1 | ../../../tq/schemas/http-batch-response-schema.json -------------------------------------------------------------------------------- /git/githistory/fixtures/identical-blobs.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | 42723ad796caa500ddf4e3f6ad37600ed5a65491 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | e669b63f829bfb0b91fc52a5bcea53dd7977a0ee 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/refs/heads/branch-a: -------------------------------------------------------------------------------- 1 | 251e6b3461a3b5adc6bab694d5ae1abc878edf85 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/refs/heads/branch-b: -------------------------------------------------------------------------------- 1 | 15805fe2044dc1a0508853e93d1a230bd94636be 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | 6c9ccaeb45446e3fa88cd5848a940fd34c18192b 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/packed-objects.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | 749f1b43e00eeb98194fedb7827b3cfb43b42b0e 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/repeated-subtrees.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | b9621d5d84b3174de020ad2c869f43b2f61f337f 2 | -------------------------------------------------------------------------------- /docs/api/schemas/http-lock-list-response-schema.json: -------------------------------------------------------------------------------- 1 | ../../../locking/schemas/http-lock-list-response-schema.json -------------------------------------------------------------------------------- /git/githistory/fixtures/non-repeated-subtrees.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | bc63077ac5e575ccc9dbbd93dc882f1e10600ea7 2 | -------------------------------------------------------------------------------- /docs/api/schemas/http-lock-create-request-schema.json: -------------------------------------------------------------------------------- 1 | ../../../locking/schemas/http-lock-create-request-schema.json -------------------------------------------------------------------------------- /docs/api/schemas/http-lock-create-response-schema.json: -------------------------------------------------------------------------------- 1 | ../../../locking/schemas/http-lock-create-response-schema.json -------------------------------------------------------------------------------- /docs/api/schemas/http-lock-delete-request-schema.json: -------------------------------------------------------------------------------- 1 | ../../../locking/schemas/http-lock-delete-request-schema.json -------------------------------------------------------------------------------- /docs/api/schemas/http-lock-verify-response-schema.json: -------------------------------------------------------------------------------- 1 | ../../../locking/schemas/http-lock-verify-response-schema.json -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | d941e4756add6b06f5bee766fcf669f55419f13f 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/refs/tags/middle: -------------------------------------------------------------------------------- 1 | 228afe30855933151f7a88e70d9d88314fd2f191 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/packed-objects.git/info/refs: -------------------------------------------------------------------------------- 1 | 749f1b43e00eeb98194fedb7827b3cfb43b42b0e refs/heads/master 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/packed-objects.git/objects/info/packs: -------------------------------------------------------------------------------- 1 | P pack-ac516ce2d006668dc5e001e8dda0aa1c7198500f.pack 2 | 3 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | d941e4756add6b06f5bee766fcf669f55419f13f 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/refs/tags/middle: -------------------------------------------------------------------------------- 1 | 05797a38b05f910e6efe40dc1a5c0a046a9403e8 2 | -------------------------------------------------------------------------------- /script/windows-installer/git-lfs-logo.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/script/windows-installer/git-lfs-logo.bmp -------------------------------------------------------------------------------- /script/windows-installer/git-lfs-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/script/windows-installer/git-lfs-logo.ico -------------------------------------------------------------------------------- /t/cmd/git-credential-lfsnoop.go: -------------------------------------------------------------------------------- 1 | //go:build testtools 2 | // +build testtools 3 | 4 | package main 5 | 6 | func main() { 7 | } 8 | -------------------------------------------------------------------------------- /git-lfs_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows && !arm64 2 | // +build windows,!arm64 3 | 4 | //go:generate goversioninfo 5 | 6 | package main 7 | -------------------------------------------------------------------------------- /tools/cygwin.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package tools 5 | 6 | func isCygwin() bool { 7 | return false 8 | } 9 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history.git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history.git/index -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/octopus-merge.git/index -------------------------------------------------------------------------------- /git/githistory/fixtures/packed-objects.git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/packed-objects.git/index -------------------------------------------------------------------------------- /git/githistory/fixtures/identical-blobs.git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/identical-blobs.git/index -------------------------------------------------------------------------------- /git/githistory/fixtures/repeated-subtrees.git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/repeated-subtrees.git/index -------------------------------------------------------------------------------- /script/compile-win-installer-unsigned.bat: -------------------------------------------------------------------------------- 1 | "%ProgramFiles(x86)%\Inno Setup 5\iscc.exe" /Qp "%~dp0\windows-installer\inno-setup-git-lfs-installer.iss" 2 | -------------------------------------------------------------------------------- /script/windows-installer/git-lfs-wizard-image.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/script/windows-installer/git-lfs-wizard-image.bmp -------------------------------------------------------------------------------- /git-lfs_windows_arm64.go: -------------------------------------------------------------------------------- 1 | //go:build windows && arm64 2 | // +build windows,arm64 3 | 4 | //go:generate goversioninfo -arm=true -64=true 5 | 6 | package main 7 | -------------------------------------------------------------------------------- /git/githistory/fixtures/non-repeated-subtrees.git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/non-repeated-subtrees.git/index -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-tags.git/index -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | -------------------------------------------------------------------------------- /tools/umask_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package tools 5 | 6 | func doWithUmask(mask int, f func() error) error { 7 | return f() 8 | } 9 | -------------------------------------------------------------------------------- /debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # The --skip-repo option prevents failure if / is a Git repository with existing 4 | # non-git-lfs hooks. 5 | git lfs install --skip-repo --system 6 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-annotated-tags.git/index -------------------------------------------------------------------------------- /tools/math.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | // ClampInt returns the integer "n" bounded between "low" and "high". 4 | func ClampInt(n, low, high int) int { 5 | return min(high, max(low, n)) 6 | } 7 | -------------------------------------------------------------------------------- /t/cmd/lfs-ssh-proxy-test.go: -------------------------------------------------------------------------------- 1 | //go:build testtools 2 | // +build testtools 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | fmt.Println("SSH PROXY TEST called") 10 | } 11 | -------------------------------------------------------------------------------- /script/install-git-source: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Builds git from a given git ref. Used for CircleCI OSX builds 3 | 4 | cd git-source 5 | git checkout "$1" 6 | make --jobs=2 7 | make install 8 | cd .. 9 | -------------------------------------------------------------------------------- /tools/humanize/package.go: -------------------------------------------------------------------------------- 1 | // package humanize is designed to parse and format "humanized" versions of 2 | // numbers with units. 3 | // 4 | // Based on: github.com/dustin/go-humanize. 5 | package humanize 6 | -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = true 8 | -------------------------------------------------------------------------------- /git/githistory/fixtures/packed-objects.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = true 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = true 8 | -------------------------------------------------------------------------------- /git/githistory/fixtures/identical-blobs.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = true 8 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = true 8 | -------------------------------------------------------------------------------- /git/githistory/fixtures/repeated-subtrees.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = true 8 | -------------------------------------------------------------------------------- /git/githistory/fixtures/identical-blobs.git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 42723ad796caa500ddf4e3f6ad37600ed5a65491 Taylor Blau 1496440063 -0600 commit (initial): initial commit 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/non-repeated-subtrees.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = true 8 | -------------------------------------------------------------------------------- /git/githistory/fixtures/packed-objects.git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 749f1b43e00eeb98194fedb7827b3cfb43b42b0e Taylor Blau 1504643527 -0400 commit (initial): *: initial commit 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = true 8 | -------------------------------------------------------------------------------- /debian/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # The --skip-repo option avoids mutating / if it is a Git repository. (Maybe the 4 | # user wants to replace this package with a different installation.) 5 | git lfs uninstall --skip-repo --system 6 | -------------------------------------------------------------------------------- /git/githistory/fixtures/identical-blobs.git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 42723ad796caa500ddf4e3f6ad37600ed5a65491 Taylor Blau 1496440063 -0600 commit (initial): initial commit 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/objects/04/df07b08ca746b3167d0f1d1514e2f39a52c16c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/octopus-merge.git/objects/04/df07b08ca746b3167d0f1d1514e2f39a52c16c -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/objects/15/805fe2044dc1a0508853e93d1a230bd94636be: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/octopus-merge.git/objects/15/805fe2044dc1a0508853e93d1a230bd94636be -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/objects/25/1e6b3461a3b5adc6bab694d5ae1abc878edf85: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/octopus-merge.git/objects/25/1e6b3461a3b5adc6bab694d5ae1abc878edf85 -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/octopus-merge.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/objects/2f/3a8ec99d5b459b41b6675e52758e397c5e3103: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/octopus-merge.git/objects/2f/3a8ec99d5b459b41b6675e52758e397c5e3103 -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/objects/5a/0581b158a81b8fc5d2169c2ac2ca7772ff13fd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/octopus-merge.git/objects/5a/0581b158a81b8fc5d2169c2ac2ca7772ff13fd -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/objects/63/d8dbd40c23542e740659a7168a0ce3138ea748: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/octopus-merge.git/objects/63/d8dbd40c23542e740659a7168a0ce3138ea748 -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/objects/6c/9ccaeb45446e3fa88cd5848a940fd34c18192b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/octopus-merge.git/objects/6c/9ccaeb45446e3fa88cd5848a940fd34c18192b -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/objects/8b/e6d64cddab01f53381e9feafe50d95ca5e6629: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/octopus-merge.git/objects/8b/e6d64cddab01f53381e9feafe50d95ca5e6629 -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/objects/b6/fc4c620b67d95f953a5c1c1230aaab5db5a1b0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/octopus-merge.git/objects/b6/fc4c620b67d95f953a5c1c1230aaab5db5a1b0 -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/objects/e9/4edfabfb7605f7cb959b4ce8fb6652b509fe03: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/octopus-merge.git/objects/e9/4edfabfb7605f7cb959b4ce8fb6652b509fe03 -------------------------------------------------------------------------------- /git/githistory/fixtures/identical-blobs.git/objects/42/723ad796caa500ddf4e3f6ad37600ed5a65491: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/identical-blobs.git/objects/42/723ad796caa500ddf4e3f6ad37600ed5a65491 -------------------------------------------------------------------------------- /git/githistory/fixtures/identical-blobs.git/objects/94/f3610c08588440112ed977376f26a8fba169b0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/identical-blobs.git/objects/94/f3610c08588440112ed977376f26a8fba169b0 -------------------------------------------------------------------------------- /git/githistory/fixtures/identical-blobs.git/objects/b6/2b45ed2b59cf32dd676ca47497e76a1dab9c7e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/identical-blobs.git/objects/b6/2b45ed2b59cf32dd676ca47497e76a1dab9c7e -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = true 8 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history.git/objects/56/a6051ca2b02b04ef92d5150c9ef600403cb1de: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history.git/objects/56/a6051ca2b02b04ef92d5150c9ef600403cb1de -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history.git/objects/62/811b8f930323895033b3b338c35f51c0b7268b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history.git/objects/62/811b8f930323895033b3b338c35f51c0b7268b -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history.git/objects/6e/07bd31cb70c4add2c973481ad4fa38b235ca69: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history.git/objects/6e/07bd31cb70c4add2c973481ad4fa38b235ca69 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history.git/objects/71/a488ec1804ee97ea651b094aa9181ca85aab0a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history.git/objects/71/a488ec1804ee97ea651b094aa9181ca85aab0a -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history.git/objects/c5/decfe1fcf39b8c489f4a0bf3b3823676339f80: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history.git/objects/c5/decfe1fcf39b8c489f4a0bf3b3823676339f80 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history.git/objects/d8/263ee9860594d2806b0dfd1bfd17528b0ba2a4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history.git/objects/d8/263ee9860594d2806b0dfd1bfd17528b0ba2a4 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history.git/objects/e4/40e5c842586965a7fb77deda2eca68612b1f53: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history.git/objects/e4/40e5c842586965a7fb77deda2eca68612b1f53 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history.git/objects/e6/69b63f829bfb0b91fc52a5bcea53dd7977a0ee: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history.git/objects/e6/69b63f829bfb0b91fc52a5bcea53dd7977a0ee -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history.git/objects/ef/eab7a9b61312fa56fc74eee1e0f5a714abfb70: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history.git/objects/ef/eab7a9b61312fa56fc74eee1e0f5a714abfb70 -------------------------------------------------------------------------------- /git/githistory/fixtures/packed-objects.git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 749f1b43e00eeb98194fedb7827b3cfb43b42b0e Taylor Blau 1504643527 -0400 commit (initial): *: initial commit 2 | -------------------------------------------------------------------------------- /git/githistory/fixtures/repeated-subtrees.git/objects/0b/4747509ab885114690ff291f8f108045b1d749: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/repeated-subtrees.git/objects/0b/4747509ab885114690ff291f8f108045b1d749 -------------------------------------------------------------------------------- /git/githistory/fixtures/repeated-subtrees.git/objects/12/b98c239e8f933d213617a1b965333d478b2743: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/repeated-subtrees.git/objects/12/b98c239e8f933d213617a1b965333d478b2743 -------------------------------------------------------------------------------- /git/githistory/fixtures/repeated-subtrees.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/repeated-subtrees.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e -------------------------------------------------------------------------------- /git/githistory/fixtures/repeated-subtrees.git/objects/47/d4d71022adc7ec6a14250d23491e535ec228f4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/repeated-subtrees.git/objects/47/d4d71022adc7ec6a14250d23491e535ec228f4 -------------------------------------------------------------------------------- /git/githistory/fixtures/repeated-subtrees.git/objects/5e/497ceceb14ad3c43bac781ed5c804bc67e8f3b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/repeated-subtrees.git/objects/5e/497ceceb14ad3c43bac781ed5c804bc67e8f3b -------------------------------------------------------------------------------- /git/githistory/fixtures/repeated-subtrees.git/objects/63/d8dbd40c23542e740659a7168a0ce3138ea748: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/repeated-subtrees.git/objects/63/d8dbd40c23542e740659a7168a0ce3138ea748 -------------------------------------------------------------------------------- /git/githistory/fixtures/repeated-subtrees.git/objects/b9/621d5d84b3174de020ad2c869f43b2f61f337f: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/repeated-subtrees.git/objects/b9/621d5d84b3174de020ad2c869f43b2f61f337f -------------------------------------------------------------------------------- /git/githistory/fixtures/repeated-subtrees.git/objects/bc/4d1181aca5a33673d7c5d4c209d09ce1cfabd7: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/repeated-subtrees.git/objects/bc/4d1181aca5a33673d7c5d4c209d09ce1cfabd7 -------------------------------------------------------------------------------- /script/macos/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "apple_id": { 3 | "password": "@env:DARWIN_DEV_PASS" 4 | }, 5 | "notarize": { 6 | "path": ["git-lfs"], 7 | "bundle_id": "com.github.git-lfs", 8 | "staple": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /git/githistory/fixtures/non-repeated-subtrees.git/objects/07/bd7fbfc41b7d36135bcffe7c465490f4aca32d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/non-repeated-subtrees.git/objects/07/bd7fbfc41b7d36135bcffe7c465490f4aca32d -------------------------------------------------------------------------------- /git/githistory/fixtures/non-repeated-subtrees.git/objects/12/7ececad475cde6da0048051d62121cabd23194: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/non-repeated-subtrees.git/objects/12/7ececad475cde6da0048051d62121cabd23194 -------------------------------------------------------------------------------- /git/githistory/fixtures/non-repeated-subtrees.git/objects/19/acdd81ab0abc15c771fe005bf1c2825e4e6080: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/non-repeated-subtrees.git/objects/19/acdd81ab0abc15c771fe005bf1c2825e4e6080 -------------------------------------------------------------------------------- /git/githistory/fixtures/non-repeated-subtrees.git/objects/37/f99c7f2706d317b3bf7ff13d574eef33d8788a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/non-repeated-subtrees.git/objects/37/f99c7f2706d317b3bf7ff13d574eef33d8788a -------------------------------------------------------------------------------- /git/githistory/fixtures/non-repeated-subtrees.git/objects/3d/1baaaceec085c52e3e57a47a75b87b7615d0ef: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/non-repeated-subtrees.git/objects/3d/1baaaceec085c52e3e57a47a75b87b7615d0ef -------------------------------------------------------------------------------- /git/githistory/fixtures/non-repeated-subtrees.git/objects/8d/14cbf983b3fad683171c9418998d9f68340823: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/non-repeated-subtrees.git/objects/8d/14cbf983b3fad683171c9418998d9f68340823 -------------------------------------------------------------------------------- /git/githistory/fixtures/non-repeated-subtrees.git/objects/bc/63077ac5e575ccc9dbbd93dc882f1e10600ea7: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/non-repeated-subtrees.git/objects/bc/63077ac5e575ccc9dbbd93dc882f1e10600ea7 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/objects/20/ecedad3e74a113695fe5f00ab003694e2e1e9c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-tags.git/objects/20/ecedad3e74a113695fe5f00ab003694e2e1e9c -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/objects/22/8afe30855933151f7a88e70d9d88314fd2f191: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-tags.git/objects/22/8afe30855933151f7a88e70d9d88314fd2f191 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-tags.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/objects/34/10062ba67c5ed59b854387a8bc0ec012479368: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-tags.git/objects/34/10062ba67c5ed59b854387a8bc0ec012479368 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/objects/3c/b3201d7942353fff5f45e03d114e8e7a061f87: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-tags.git/objects/3c/b3201d7942353fff5f45e03d114e8e7a061f87 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/objects/52/a8963f48d54c7d352695a278ca4b025e130cb4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-tags.git/objects/52/a8963f48d54c7d352695a278ca4b025e130cb4 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/objects/63/d8dbd40c23542e740659a7168a0ce3138ea748: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-tags.git/objects/63/d8dbd40c23542e740659a7168a0ce3138ea748 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/objects/91/b85be6928569390e937479509b80a1d0dccb0c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-tags.git/objects/91/b85be6928569390e937479509b80a1d0dccb0c -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/objects/d9/41e4756add6b06f5bee766fcf669f55419f13f: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-tags.git/objects/d9/41e4756add6b06f5bee766fcf669f55419f13f -------------------------------------------------------------------------------- /git/githistory/fixtures/packed-objects.git/objects/pack/pack-ac516ce2d006668dc5e001e8dda0aa1c7198500f.idx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/packed-objects.git/objects/pack/pack-ac516ce2d006668dc5e001e8dda0aa1c7198500f.idx -------------------------------------------------------------------------------- /git/githistory/fixtures/packed-objects.git/objects/pack/pack-ac516ce2d006668dc5e001e8dda0aa1c7198500f.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/packed-objects.git/objects/pack/pack-ac516ce2d006668dc5e001e8dda0aa1c7198500f.pack -------------------------------------------------------------------------------- /tools/umask_nix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package tools 5 | 6 | import "syscall" 7 | 8 | func doWithUmask(mask int, f func() error) error { 9 | mask = syscall.Umask(mask) 10 | defer syscall.Umask(mask) 11 | return f() 12 | } 13 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/05/797a38b05f910e6efe40dc1a5c0a046a9403e8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/05/797a38b05f910e6efe40dc1a5c0a046a9403e8 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/20/ecedad3e74a113695fe5f00ab003694e2e1e9c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/20/ecedad3e74a113695fe5f00ab003694e2e1e9c -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/22/8afe30855933151f7a88e70d9d88314fd2f191: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/22/8afe30855933151f7a88e70d9d88314fd2f191 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/34/10062ba67c5ed59b854387a8bc0ec012479368: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/34/10062ba67c5ed59b854387a8bc0ec012479368 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/3c/b3201d7942353fff5f45e03d114e8e7a061f87: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/3c/b3201d7942353fff5f45e03d114e8e7a061f87 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/4a/78e180c45f18489941174df19d538c26d5318b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/4a/78e180c45f18489941174df19d538c26d5318b -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/52/a8963f48d54c7d352695a278ca4b025e130cb4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/52/a8963f48d54c7d352695a278ca4b025e130cb4 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/63/d8dbd40c23542e740659a7168a0ce3138ea748: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/63/d8dbd40c23542e740659a7168a0ce3138ea748 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/6b/de0b381fa1a039396445e2ce5a28c0451fde15: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/6b/de0b381fa1a039396445e2ce5a28c0451fde15 -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/91/b85be6928569390e937479509b80a1d0dccb0c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/91/b85be6928569390e937479509b80a1d0dccb0c -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/d9/41e4756add6b06f5bee766fcf669f55419f13f: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-lfs/git-lfs/HEAD/git/githistory/fixtures/linear-history-with-annotated-tags.git/objects/d9/41e4756add6b06f5bee766fcf669f55419f13f -------------------------------------------------------------------------------- /config/util_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package config 5 | 6 | // Windows doesn't provide the umask syscall, so return something sane as a 7 | // default. os.Chmod will only care about the owner bits anyway. 8 | func umask() int { 9 | return 077 10 | } 11 | -------------------------------------------------------------------------------- /commands/path_nix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package commands 5 | 6 | // cleanRootPath is a no-op on every platform except Windows 7 | func cleanRootPath(pattern string) string { 8 | return pattern 9 | } 10 | 11 | func osLineEnding() string { 12 | return "\n" 13 | } 14 | -------------------------------------------------------------------------------- /docs/man/git-lfs-env.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-env(1) 2 | 3 | == NAME 4 | 5 | git-lfs-env - Display the Git LFS environment 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs env` 10 | 11 | == DESCRIPTION 12 | 13 | Display the current Git LFS environment. 14 | 15 | == SEE ALSO 16 | 17 | Part of the git-lfs(1) suite. 18 | -------------------------------------------------------------------------------- /tools/robustio.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package tools 5 | 6 | import "os" 7 | 8 | func RobustRename(oldpath, newpath string) error { 9 | return os.Rename(oldpath, newpath) 10 | } 11 | 12 | func RobustOpen(name string) (*os.File, error) { 13 | return os.Open(name) 14 | } 15 | -------------------------------------------------------------------------------- /config/util_nix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package config 5 | 6 | import "syscall" 7 | 8 | func umask() int { 9 | // umask(2), which this function wraps, also sets the umask, so set it 10 | // back. 11 | umask := syscall.Umask(022) 12 | syscall.Umask(umask) 13 | return umask 14 | } 15 | -------------------------------------------------------------------------------- /debian/git-lfs.lintian-overrides: -------------------------------------------------------------------------------- 1 | # Go only produces static binaries so read-only relocations aren't possible 2 | hardening-no-relro usr/bin/git-lfs 3 | 4 | # strip disabled as golang upstream doesn't support it and it makes go 5 | # crash. See https://launchpad.net/bugs/1200255. 6 | unstripped-binary-or-object usr/bin/git-lfs 7 | -------------------------------------------------------------------------------- /tools/filetools_nix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package tools 5 | 6 | import "path/filepath" 7 | 8 | func CanonicalizeSystemPath(path string) (string, error) { 9 | path, err := filepath.Abs(path) 10 | if err != nil { 11 | return "", err 12 | } 13 | return filepath.EvalSymlinks(path) 14 | } 15 | -------------------------------------------------------------------------------- /t/cmd/lfstest-badpathcheck.go: -------------------------------------------------------------------------------- 1 | //go:build testtools 2 | // +build testtools 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | ) 10 | 11 | func main() { 12 | fmt.Println("exploit") 13 | fmt.Fprintln(os.Stderr, "exploit") 14 | 15 | f, err := os.Create("exploit") 16 | if err != nil { 17 | f.Close() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /commands/path.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import "strings" 4 | 5 | func gitLineEnding(git env) string { 6 | value, _ := git.Get("core.autocrlf") 7 | switch strings.ToLower(value) { 8 | case "true", "t", "1": 9 | return "\r\n" 10 | default: 11 | return osLineEnding() 12 | } 13 | } 14 | 15 | type env interface { 16 | Get(string) (string, bool) 17 | } 18 | -------------------------------------------------------------------------------- /commands/pointers.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import "github.com/git-lfs/git-lfs/v3/lfs" 4 | 5 | func collectPointers(pointerCh *lfs.PointerChannelWrapper) ([]*lfs.WrappedPointer, error) { 6 | var pointers []*lfs.WrappedPointer 7 | for p := range pointerCh.Results { 8 | pointers = append(pointers, p) 9 | } 10 | return pointers, pointerCh.Wait() 11 | } 12 | -------------------------------------------------------------------------------- /git/githistory/fixtures/repeated-subtrees.git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 0b4747509ab885114690ff291f8f108045b1d749 Taylor Blau 1496362788 -0600 commit (initial): initial commit 2 | 0b4747509ab885114690ff291f8f108045b1d749 b9621d5d84b3174de020ad2c869f43b2f61f337f Taylor Blau 1496362801 -0600 commit: a.txt: changes 3 | -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/logs/refs/heads/branch-a: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 8be6d64cddab01f53381e9feafe50d95ca5e6629 Taylor Blau 1496422012 -0600 branch: Created from HEAD 2 | 8be6d64cddab01f53381e9feafe50d95ca5e6629 251e6b3461a3b5adc6bab694d5ae1abc878edf85 Taylor Blau 1496422020 -0600 commit: a.txt: initial 3 | -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/logs/refs/heads/branch-b: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 8be6d64cddab01f53381e9feafe50d95ca5e6629 Taylor Blau 1496422029 -0600 branch: Created from HEAD 2 | 8be6d64cddab01f53381e9feafe50d95ca5e6629 15805fe2044dc1a0508853e93d1a230bd94636be Taylor Blau 1496422035 -0600 commit: b.txt: initial 3 | -------------------------------------------------------------------------------- /subprocess/buffered_cmd.go: -------------------------------------------------------------------------------- 1 | package subprocess 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | ) 7 | 8 | const ( 9 | // stdoutBufSize is the size of the buffers given to a sub-process stdout 10 | stdoutBufSize = 16384 11 | ) 12 | 13 | type BufferedCmd struct { 14 | *Cmd 15 | 16 | Stdin io.WriteCloser 17 | Stdout *bufio.Reader 18 | Stderr *bufio.Reader 19 | } 20 | -------------------------------------------------------------------------------- /git/githistory/fixtures/repeated-subtrees.git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 0b4747509ab885114690ff291f8f108045b1d749 Taylor Blau 1496362788 -0600 commit (initial): initial commit 2 | 0b4747509ab885114690ff291f8f108045b1d749 b9621d5d84b3174de020ad2c869f43b2f61f337f Taylor Blau 1496362801 -0600 commit: a.txt: changes 3 | -------------------------------------------------------------------------------- /docs/proposals/README.md: -------------------------------------------------------------------------------- 1 | # Git LFS Proposals 2 | 3 | This directory contains high level proposals for future Git LFS features. 4 | Inclusion here does not guarantee when or if a feature will make it in to Git 5 | LFS. It doesn't even guarantee that the specifics won't change. 6 | 7 | Everyone is welcome to submit their own proposal as a markdown file in a 8 | pull request for discussion. 9 | -------------------------------------------------------------------------------- /git/config_test.go: -------------------------------------------------------------------------------- 1 | package git_test // to avoid import cycles 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/git-lfs/git-lfs/v3/git" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestReadOnlyConfig(t *testing.T) { 11 | cfg := NewReadOnlyConfig("", "") 12 | _, err := cfg.SetLocal("lfs.this.should", "fail") 13 | assert.Equal(t, err, ErrReadOnly) 14 | } 15 | -------------------------------------------------------------------------------- /git/githistory/fixtures/non-repeated-subtrees.git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 37f99c7f2706d317b3bf7ff13d574eef33d8788a Taylor Blau 1496686519 -0600 commit (initial): a.txt: initial commit 2 | 37f99c7f2706d317b3bf7ff13d574eef33d8788a bc63077ac5e575ccc9dbbd93dc882f1e10600ea7 Taylor Blau 1496686541 -0600 commit: subdir/b.txt: initial commit 3 | -------------------------------------------------------------------------------- /git/githistory/fixtures/non-repeated-subtrees.git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 37f99c7f2706d317b3bf7ff13d574eef33d8788a Taylor Blau 1496686519 -0600 commit (initial): a.txt: initial commit 2 | 37f99c7f2706d317b3bf7ff13d574eef33d8788a bc63077ac5e575ccc9dbbd93dc882f1e10600ea7 Taylor Blau 1496686541 -0600 commit: subdir/b.txt: initial commit 3 | -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 8be6d64cddab01f53381e9feafe50d95ca5e6629 Taylor Blau 1496421999 -0600 commit (initial): initial commit 2 | 8be6d64cddab01f53381e9feafe50d95ca5e6629 6c9ccaeb45446e3fa88cd5848a940fd34c18192b Taylor Blau 1496422044 -0600 merge branch-a branch-b: Merge made by the 'octopus' strategy. 3 | -------------------------------------------------------------------------------- /po/es.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 4 | "MIME-Version: 1.0\n" 5 | "Content-Type: text/plain; charset=UTF-8\n" 6 | "Content-Transfer-Encoding: 8bit\n" 7 | "Language: \n" 8 | "X-Generator: xgotext\n" 9 | 10 | #: command_filter_process.go:40 11 | msgid "This command should be run by the Git filter process" 12 | msgstr "Este comando debe ser invocado por el proceso de filtración de Git" -------------------------------------------------------------------------------- /versioninfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "FixedFileInfo": 3 | { 4 | "FileVersion": { 5 | "Major": 3, 6 | "Minor": 7, 7 | "Patch": 0, 8 | "Build": 0 9 | } 10 | }, 11 | "StringFileInfo": 12 | { 13 | "FileDescription": "Git LFS", 14 | "LegalCopyright": "GitHub, Inc. and Git LFS contributors", 15 | "ProductName": "Git Large File Storage (LFS)", 16 | "ProductVersion": "3.7.0" 17 | }, 18 | "IconPath": "script/windows-installer/git-lfs-logo.ico" 19 | } 20 | -------------------------------------------------------------------------------- /tools/math_test.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestClampDiscardsIntsLowerThanMin(t *testing.T) { 10 | assert.Equal(t, 0, ClampInt(-1, 0, 1)) 11 | } 12 | 13 | func TestClampDiscardsIntsGreaterThanMax(t *testing.T) { 14 | assert.Equal(t, 1, ClampInt(2, 0, 1)) 15 | } 16 | 17 | func TestClampAcceptsIntsWithinBounds(t *testing.T) { 18 | assert.Equal(t, 1, ClampInt(1, 0, 2)) 19 | } 20 | -------------------------------------------------------------------------------- /locking/schemas/http-lock-delete-request-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema", 3 | "title": "Git LFS HTTPS Lock Deletion API Request", 4 | "type": "object", 5 | "properties": { 6 | "force": { 7 | "type": "boolean" 8 | }, 9 | "ref": { 10 | "type": "object", 11 | "properties": { 12 | "name": { 13 | "type": "string" 14 | } 15 | }, 16 | "required": ["name"] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Git LFS Documentation 2 | 3 | ## Reference Manual 4 | 5 | Each Git LFS subcommand is documented in the [official man pages](man). Any of 6 | these can also be viewed from the command line: 7 | 8 | ```bash 9 | $ git lfs help 10 | $ git lfs -h 11 | ``` 12 | 13 | ## Developer Docs 14 | 15 | Details of how the Git LFS **client** works are in the [official specification](spec.md). 16 | 17 | Details of how the GIT LFS **server** works are in the [API specification](api). 18 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history.git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 62811b8f930323895033b3b338c35f51c0b7268b Taylor Blau 1496347620 -0600 commit (initial): hello.txt: 1 2 | 62811b8f930323895033b3b338c35f51c0b7268b efeab7a9b61312fa56fc74eee1e0f5a714abfb70 Taylor Blau 1496347630 -0600 commit: hello.txt: 2 3 | efeab7a9b61312fa56fc74eee1e0f5a714abfb70 e669b63f829bfb0b91fc52a5bcea53dd7977a0ee Taylor Blau 1496347641 -0600 commit: hello.txt: 3 4 | -------------------------------------------------------------------------------- /script/notarize: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Notarizes the file given on the command line with the Apple ID in 4 | # $DARWIN_DEV_USER, the password in $DARWIN_DEV_PASS, and the team ID (usually 5 | # ten characters) in $DARWIN_DEV_TEAM. 6 | # 7 | # This script exists to not echo these variables into the log. Don't run this 8 | # on a multi-user system, only in CI. 9 | 10 | xcrun notarytool submit "$1" \ 11 | --apple-id "$DARWIN_DEV_USER" --password "$DARWIN_DEV_PASS" --team-id "$DARWIN_DEV_TEAM" \ 12 | --wait 13 | -------------------------------------------------------------------------------- /subprocess/subprocess_nix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package subprocess 5 | 6 | import ( 7 | "os/exec" 8 | ) 9 | 10 | // ExecCommand is a small platform specific wrapper around os/exec.Command 11 | func ExecCommand(name string, arg ...string) (*Cmd, error) { 12 | cmd := exec.Command(name, arg...) 13 | var err error 14 | cmd.Path, err = LookPath(name) 15 | if err != nil { 16 | return nil, err 17 | } 18 | cmd.Env = fetchEnvironment() 19 | return newCmd(cmd), nil 20 | } 21 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 91b85be6928569390e937479509b80a1d0dccb0c Taylor Blau 1496954196 -0600 commit (initial): some.txt: a 2 | 91b85be6928569390e937479509b80a1d0dccb0c 228afe30855933151f7a88e70d9d88314fd2f191 Taylor Blau 1496954207 -0600 commit: some.txt: b 3 | 228afe30855933151f7a88e70d9d88314fd2f191 d941e4756add6b06f5bee766fcf669f55419f13f Taylor Blau 1496954214 -0600 commit: some.txt: c 4 | -------------------------------------------------------------------------------- /locking/schemas/http-lock-create-request-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema", 3 | "title": "Git LFS HTTPS Lock Creation API Request", 4 | "type": "object", 5 | "properties": { 6 | "path": { 7 | "type": "string" 8 | }, 9 | "ref": { 10 | "type": "object", 11 | "properties": { 12 | "name": { 13 | "type": "string" 14 | } 15 | }, 16 | "required": ["name"] 17 | } 18 | }, 19 | "required": ["path"] 20 | } 21 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history.git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 62811b8f930323895033b3b338c35f51c0b7268b Taylor Blau 1496347620 -0600 commit (initial): hello.txt: 1 2 | 62811b8f930323895033b3b338c35f51c0b7268b efeab7a9b61312fa56fc74eee1e0f5a714abfb70 Taylor Blau 1496347630 -0600 commit: hello.txt: 2 3 | efeab7a9b61312fa56fc74eee1e0f5a714abfb70 e669b63f829bfb0b91fc52a5bcea53dd7977a0ee Taylor Blau 1496347641 -0600 commit: hello.txt: 3 4 | -------------------------------------------------------------------------------- /t/cmd/lfstest-nanomtime.go: -------------------------------------------------------------------------------- 1 | //go:build testtools 2 | // +build testtools 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | ) 10 | 11 | func main() { 12 | if len(os.Args) < 2 { 13 | fmt.Fprintf(os.Stderr, "Need an argument") 14 | os.Exit(2) 15 | } 16 | st, err := os.Stat(os.Args[1]) 17 | if err != nil { 18 | fmt.Fprintf(os.Stderr, "Failed to stat %q: %s", os.Args[1], err) 19 | os.Exit(3) 20 | } 21 | mtime := st.ModTime() 22 | fmt.Printf("%d.%09d", mtime.Unix(), mtime.Nanosecond()) 23 | } 24 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 91b85be6928569390e937479509b80a1d0dccb0c Taylor Blau 1496954196 -0600 commit (initial): some.txt: a 2 | 91b85be6928569390e937479509b80a1d0dccb0c 228afe30855933151f7a88e70d9d88314fd2f191 Taylor Blau 1496954207 -0600 commit: some.txt: b 3 | 228afe30855933151f7a88e70d9d88314fd2f191 d941e4756add6b06f5bee766fcf669f55419f13f Taylor Blau 1496954214 -0600 commit: some.txt: c 4 | -------------------------------------------------------------------------------- /docs/man/git-lfs-post-merge.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-post-merge(1) 2 | 3 | == NAME 4 | 5 | git-lfs-post-merge - Git post-merge hook implementation 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs post-merge` 10 | 11 | == DESCRIPTION 12 | 13 | Responds to Git post-merge events. It makes sure that any files which 14 | are marked as lockable by `git lfs track` are read-only in the working 15 | copy, if not currently locked by the local user. 16 | 17 | == SEE ALSO 18 | 19 | git-lfs-track(1) 20 | 21 | Part of the git-lfs(1) suite. 22 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-tags.git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 91b85be6928569390e937479509b80a1d0dccb0c Taylor Blau 1496954196 -0600 commit (initial): some.txt: a 2 | 91b85be6928569390e937479509b80a1d0dccb0c 228afe30855933151f7a88e70d9d88314fd2f191 Taylor Blau 1496954207 -0600 commit: some.txt: b 3 | 228afe30855933151f7a88e70d9d88314fd2f191 d941e4756add6b06f5bee766fcf669f55419f13f Taylor Blau 1496954214 -0600 commit: some.txt: c 4 | -------------------------------------------------------------------------------- /commands/command_standalone_file.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/git-lfs/git-lfs/v3/lfshttp/standalone" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func standaloneFileCommand(cmd *cobra.Command, args []string) { 12 | err := standalone.ProcessStandaloneData(cfg, os.Stdin, os.Stdout) 13 | if err != nil { 14 | fmt.Fprintln(os.Stderr, err.Error()) 15 | os.Exit(2) 16 | } 17 | } 18 | 19 | func init() { 20 | RegisterCommand("standalone-file", standaloneFileCommand, nil) 21 | } 22 | -------------------------------------------------------------------------------- /git/githistory/fixtures/linear-history-with-annotated-tags.git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 91b85be6928569390e937479509b80a1d0dccb0c Taylor Blau 1496954196 -0600 commit (initial): some.txt: a 2 | 91b85be6928569390e937479509b80a1d0dccb0c 228afe30855933151f7a88e70d9d88314fd2f191 Taylor Blau 1496954207 -0600 commit: some.txt: b 3 | 228afe30855933151f7a88e70d9d88314fd2f191 d941e4756add6b06f5bee766fcf669f55419f13f Taylor Blau 1496954214 -0600 commit: some.txt: c 4 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: git-lfs 2 | Section: vcs 3 | Priority: optional 4 | Maintainer: Stephen Gelman 5 | Build-Depends: debhelper (>= 9), dh-golang, golang-go:native (>= 1.23.0), git (>= 2.0.0), asciidoctor 6 | Standards-Version: 3.9.6 7 | 8 | Package: git-lfs 9 | Architecture: any 10 | Depends: ${shlibs:Depends}, ${misc:Depends}, git (>= 2.0.0) 11 | Built-Using: ${misc:Built-Using} 12 | Description: Git Large File Support 13 | An open source Git extension for versioning large files 14 | Homepage: https://git-lfs.com/ 15 | -------------------------------------------------------------------------------- /commands/multiwriter.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | type multiWriter struct { 9 | writer io.Writer 10 | fd uintptr 11 | } 12 | 13 | func newMultiWriter(f *os.File, writers ...io.Writer) *multiWriter { 14 | return &multiWriter{ 15 | writer: io.MultiWriter(append([]io.Writer{f}, writers...)...), 16 | fd: f.Fd(), 17 | } 18 | } 19 | 20 | func (w *multiWriter) Write(p []byte) (n int, err error) { 21 | return w.writer.Write(p) 22 | } 23 | 24 | func (w *multiWriter) Fd() uintptr { 25 | return w.fd 26 | } 27 | -------------------------------------------------------------------------------- /lfsapi/kerberos.go: -------------------------------------------------------------------------------- 1 | package lfsapi 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/git-lfs/git-lfs/v3/creds" 7 | ) 8 | 9 | func (c *Client) doWithNegotiate(req *http.Request, credWrapper creds.CredentialHelperWrapper) (*http.Response, error) { 10 | // There are two possibilities here if we're using Negotiate 11 | // authentication. One is that we're using Kerberos, which we try 12 | // first. The other is that we're using NTLM, which we no longer 13 | // support. Fail in that case. 14 | return c.doWithAccess(req, "", nil, creds.NegotiateAccess) 15 | } 16 | -------------------------------------------------------------------------------- /t/t-push-file-with-branch-name.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "push a file with the same name as a branch" 6 | ( 7 | set -e 8 | 9 | reponame="$(basename "$0" ".sh")" 10 | setup_remote_repo "$reponame" 11 | clone_repo "$reponame" repo 12 | 13 | git lfs track "main" 14 | echo "main" > main 15 | git add .gitattributes main 16 | git commit -m "add main" 17 | 18 | git lfs push --all origin main 2>&1 | tee push.log 19 | grep "Uploading LFS objects: 100% (1/1), [0-9] B" push.log 20 | ) 21 | end_test 22 | -------------------------------------------------------------------------------- /t/t-logs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "logs" 6 | ( 7 | set -e 8 | 9 | mkdir logs 10 | cd logs 11 | git init 12 | 13 | boomtownExit="" 14 | set +e 15 | git lfs logs boomtown 16 | boomtownExit=$? 17 | set -e 18 | 19 | [ "$boomtownExit" = "2" ] 20 | 21 | logname=`ls .git/lfs/logs` 22 | logfile=".git/lfs/logs/$logname" 23 | cat "$logfile" 24 | echo "... grep ..." 25 | grep "$ git-lfs logs boomtown" "$logfile" 26 | 27 | [ "$(cat "$logfile")" = "$(git lfs logs last)" ] 28 | ) 29 | end_test 30 | -------------------------------------------------------------------------------- /docs/man/git-lfs-post-checkout.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-post-checkout(1) 2 | 3 | == NAME 4 | 5 | git-lfs-post-checkout - Git post-checkout hook implementation 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs post-checkout` 10 | 11 | == DESCRIPTION 12 | 13 | Responds to Git post-checkout events. It makes sure that any files which 14 | are marked as lockable by `git lfs track` are read-only in the working 15 | copy, if not currently locked by the local user. 16 | 17 | == SEE ALSO 18 | 19 | git-lfs-track(1) 20 | 21 | Part of the git-lfs(1) suite. 22 | -------------------------------------------------------------------------------- /docs/man/git-lfs-untrack.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-untrack(1) 2 | 3 | == NAME 4 | 5 | git-lfs-untrack - Remove Git LFS paths from Git Attributes 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs untrack` ... 10 | 11 | == DESCRIPTION 12 | 13 | Stop tracking the given path(s) through Git LFS. The argument can be a 14 | glob pattern or a file path. 15 | 16 | == EXAMPLES 17 | 18 | * Configure Git LFS to stop tracking GIF files: 19 | + 20 | `git lfs untrack "*.gif"` 21 | 22 | == SEE ALSO 23 | 24 | git-lfs-track(1), git-lfs-install(1), gitattributes(5). 25 | 26 | Part of the git-lfs(1) suite. 27 | -------------------------------------------------------------------------------- /subprocess/subprocess_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package subprocess 5 | 6 | import ( 7 | "os/exec" 8 | "syscall" 9 | ) 10 | 11 | // ExecCommand is a small platform specific wrapper around os/exec.Command 12 | func ExecCommand(name string, arg ...string) (*Cmd, error) { 13 | cmd := exec.Command(name, arg...) 14 | var err error 15 | cmd.Path, err = LookPath(name) 16 | if err != nil { 17 | return nil, err 18 | } 19 | cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} 20 | cmd.Env = fetchEnvironment() 21 | return newCmd(cmd), nil 22 | } 23 | -------------------------------------------------------------------------------- /tools/util_generic.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !darwin && !windows 2 | // +build !linux,!darwin,!windows 3 | 4 | package tools 5 | 6 | import ( 7 | "io" 8 | 9 | "github.com/git-lfs/git-lfs/v3/errors" 10 | "github.com/git-lfs/git-lfs/v3/tr" 11 | ) 12 | 13 | func CheckCloneFileSupported(dir string) (supported bool, err error) { 14 | return false, errors.New(tr.Tr.Get("unsupported platform")) 15 | } 16 | 17 | func CloneFile(writer io.Writer, reader io.Reader) (bool, error) { 18 | return false, nil 19 | } 20 | 21 | func CloneFileByPath(_, _ string) (bool, error) { 22 | return false, nil 23 | } 24 | -------------------------------------------------------------------------------- /commands/command_version.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/git-lfs/git-lfs/v3/lfshttp" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var ( 9 | lovesComics bool 10 | ) 11 | 12 | func versionCommand(cmd *cobra.Command, args []string) { 13 | Print(lfshttp.UserAgent) 14 | 15 | if lovesComics { 16 | Print("Nothing may see Gah Lak Tus and survive!") 17 | } 18 | } 19 | 20 | func init() { 21 | RegisterCommand("version", versionCommand, func(cmd *cobra.Command) { 22 | cmd.PreRun = nil 23 | cmd.Flags().BoolVarP(&lovesComics, "comics", "c", false, "easter egg") 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /docs/man/git-lfs-ext.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-ext(1) 2 | 3 | == NAME 4 | 5 | git-lfs-ext - View extension details 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs ext list` [...] 10 | 11 | == DESCRIPTION 12 | 13 | Git LFS extensions enable the manipulation of files streams during 14 | smudge and clean. 15 | 16 | == EXAMPLES 17 | 18 | * List details for all extensions 19 | + 20 | .... 21 | $ git lfs ext 22 | $ git lfs ext list 23 | .... 24 | * List details for the specified extensions 25 | + 26 | .... 27 | $ git lfs ext list 'foo' 'bar' 28 | .... 29 | 30 | == SEE ALSO 31 | 32 | Part of the git-lfs(1) suite. 33 | -------------------------------------------------------------------------------- /t/t-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "git lfs --version is a synonym of git lfs version" 6 | ( 7 | set -e 8 | 9 | reponame="git-lfs-version-synonymous" 10 | mkdir "$reponame" 11 | cd "$reponame" 12 | 13 | git lfs version 2>&1 >version.log 14 | git lfs --version 2>&1 >flag.log 15 | 16 | if [ "$(cat version.log)" != "$(cat flag.log)" ]; then 17 | echo >&2 "fatal: expected 'git lfs version' and 'git lfs --version' to" 18 | echo >&2 "produce identical output ..." 19 | 20 | diff -u {version,flag}.log 21 | fi 22 | ) 23 | end_test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | benchmark/ 3 | out/ 4 | resource.syso 5 | 6 | man/* 7 | 8 | *.test 9 | tmp 10 | t/remote 11 | t/scutiger 12 | t/test_count 13 | t/test_count.lock 14 | 15 | debian/git-lfs/ 16 | debian/*.log 17 | debian/files 18 | debian/*.substvars 19 | debian/debhelper-build-stamp 20 | debian/.debhelper 21 | /.pc 22 | obj-* 23 | 24 | rpm/BUILD* 25 | rpm/*RPMS 26 | rpm/*.log 27 | rpm/SOURCES 28 | 29 | repos 30 | docker/*.key 31 | 32 | src 33 | commands/mancontent_gen.go 34 | 35 | po/build 36 | po/i-reverse.po 37 | *.mo 38 | *.pot 39 | tr/tr_gen.go 40 | 41 | lfstest-* 42 | !lfstest-*.go 43 | 44 | vendor/ 45 | -------------------------------------------------------------------------------- /t/cmd/lfs-askpass.go: -------------------------------------------------------------------------------- 1 | //go:build testtools 2 | // +build testtools 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func main() { 13 | prompt := strings.Join(os.Args[1:], " ") 14 | 15 | var answer string 16 | 17 | if strings.Contains(prompt, "Username") { 18 | answer = "user" 19 | if env, ok := os.LookupEnv("LFS_ASKPASS_USERNAME"); ok { 20 | answer = env 21 | } 22 | } else if strings.Contains(prompt, "Password") { 23 | answer = "pass" 24 | if env, ok := os.LookupEnv("LFS_ASKPASS_PASSWORD"); ok { 25 | answer = env 26 | } 27 | } 28 | 29 | fmt.Println(answer) 30 | } 31 | -------------------------------------------------------------------------------- /config/version.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | GitCommit string 11 | VersionDesc string 12 | Vendor string 13 | ) 14 | 15 | const ( 16 | Version = "3.7.0" 17 | ) 18 | 19 | func init() { 20 | gitCommit := "" 21 | if len(GitCommit) > 0 { 22 | gitCommit = "; git " + GitCommit 23 | } 24 | if len(Vendor) == 0 { 25 | Vendor = "GitHub" 26 | } 27 | VersionDesc = fmt.Sprintf("git-lfs/%s (%s; %s %s; go %s%s)", 28 | Version, 29 | Vendor, 30 | runtime.GOOS, 31 | runtime.GOARCH, 32 | strings.Replace(runtime.Version(), "go", "", 1), 33 | gitCommit, 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /t/t-object-authenticated.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | # these tests rely on GIT_TERMINAL_PROMPT to test properly 6 | ensure_git_version_isnt $VERSION_LOWER "2.3.0" 7 | 8 | begin_test "download authenticated object" 9 | ( 10 | set -e 11 | reponame="$(basename "$0" ".sh")" 12 | setup_remote_repo "$reponame" 13 | clone_repo "$reponame" without-creds 14 | 15 | git lfs track "*.dat" 16 | printf "object-authenticated" > hi.dat 17 | git add hi.dat 18 | git add .gitattributes 19 | git commit -m "initial commit" 20 | 21 | GIT_CURL_VERBOSE=1 GIT_TERMINAL_PROMPT=0 git lfs push origin main 22 | ) 23 | end_test 24 | -------------------------------------------------------------------------------- /t/t-clone-deprecated.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | ensure_git_version_isnt $VERSION_LOWER "2.15.0" 6 | 7 | begin_test "clone (deprecated on new versions of Git)" 8 | ( 9 | set -e 10 | 11 | reponame="clone-deprecated-recent-versions" 12 | setup_remote_repo "$reponame" 13 | 14 | mkdir -p "$reponame" 15 | pushd "$reponame" > /dev/null 16 | git lfs clone "$GITSERVER/$reponame" 2>&1 | tee clone.log 17 | grep "WARNING: \`git lfs clone\` is deprecated and will not be updated" clone.log 18 | grep "\`git clone\` has been updated in upstream Git to have comparable" clone.log 19 | popd > /dev/null 20 | ) 21 | end_test 22 | -------------------------------------------------------------------------------- /tq/errors_test.go: -------------------------------------------------------------------------------- 1 | package tq 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMissingObjectErrorsAreRecognizable(t *testing.T) { 10 | err := newObjectMissingError("some-name", "some-oid").(*MalformedObjectError) 11 | 12 | assert.Equal(t, "some-name", err.Name) 13 | assert.Equal(t, "some-oid", err.Oid) 14 | assert.True(t, err.Missing()) 15 | } 16 | 17 | func TestCorruptObjectErrorsAreRecognizable(t *testing.T) { 18 | err := newCorruptObjectError("some-name", "some-oid").(*MalformedObjectError) 19 | 20 | assert.Equal(t, "some-name", err.Name) 21 | assert.Equal(t, "some-oid", err.Oid) 22 | assert.True(t, err.Corrupt()) 23 | } 24 | -------------------------------------------------------------------------------- /t/t-unusual-filenames.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | reponame="$(basename "$0" ".sh")" 6 | 7 | # Leading dashes may be misinterpreted as flags if commands don't use "--" 8 | # before paths. 9 | name1='-dash.dat' 10 | contents1='leading dash' 11 | 12 | begin_test "push unusually named files" 13 | ( 14 | set -e 15 | 16 | setup_remote_repo "$reponame" 17 | 18 | clone_repo "$reponame" repo 19 | 20 | git lfs track "*.dat" 21 | echo "$content1" > "$name1" 22 | 23 | git add -- .gitattributes *.dat 24 | git commit -m "add files" 25 | 26 | git push origin main | tee push.log 27 | grep "Uploading LFS objects: 100% (1/1), 1 B" push.log 28 | ) 29 | end_test 30 | -------------------------------------------------------------------------------- /t/t-progress-meter.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "progress meter displays positive progress" 6 | ( 7 | set -e 8 | 9 | reponame="progress-meter" 10 | setup_remote_repo "$reponame" 11 | clone_repo "$reponame" "$reponame" 12 | 13 | git lfs track "*.dat" 14 | git add .gitattributes 15 | git commit -m "initial commit" 16 | 17 | for i in `seq 1 128`; do 18 | printf "%s" "$i" > "$i.dat" 19 | done 20 | 21 | git add *.dat 22 | git commit -m "add many objects" 23 | 24 | git push origin main 2>&1 | tee push.log 25 | [ "0" -eq "${PIPESTATUS[0]}" ] 26 | 27 | grep "Uploading LFS objects: 100% (128/128), 276 B" push.log 28 | ) 29 | end_test 30 | -------------------------------------------------------------------------------- /lfshttp/cookies.go: -------------------------------------------------------------------------------- 1 | package lfshttp 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/git-lfs/git-lfs/v3/tools" 8 | "github.com/ssgelm/cookiejarparser" 9 | ) 10 | 11 | func isCookieJarEnabledForHost(c *Client, host string) bool { 12 | _, cookieFileOk := c.uc.Get("http", fmt.Sprintf("https://%v", host), "cookieFile") 13 | 14 | return cookieFileOk 15 | } 16 | 17 | func getCookieJarForHost(c *Client, host string) (http.CookieJar, error) { 18 | cookieFile, _ := c.uc.Get("http", fmt.Sprintf("https://%v", host), "cookieFile") 19 | 20 | cookieFilePath, err := tools.ExpandPath(cookieFile, false) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return cookiejarparser.LoadCookieJarFile(cookieFilePath) 26 | } 27 | -------------------------------------------------------------------------------- /t/t-uninstall-worktree-unsupported.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | # These tests rely on behavior found in Git versions less than 2.20.0 to 6 | # perform themselves, specifically: 7 | # - lack of worktreeConfig extension support 8 | ensure_git_version_isnt $VERSION_HIGHER "2.20.0" 9 | 10 | begin_test "uninstall --worktree with unsupported worktreeConfig extension" 11 | ( 12 | set -e 13 | 14 | reponame="$(basename "$0" ".sh")-unsupported" 15 | mkdir "$reponame" 16 | cd "$reponame" 17 | 18 | set +e 19 | git lfs uninstall --worktree 2>err.log 20 | res=$? 21 | set -e 22 | 23 | cat err.log 24 | grep -i "error" err.log 25 | grep -- "--worktree" err.log 26 | [ "0" != "$res" ] 27 | ) 28 | end_test 29 | -------------------------------------------------------------------------------- /t/t-install-worktree-unsupported.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | # These tests rely on behavior found in Git versions less than 2.20.0 to 6 | # perform themselves, specifically: 7 | # - lack of worktreeConfig extension support 8 | ensure_git_version_isnt $VERSION_HIGHER "2.20.0" 9 | 10 | begin_test "install --worktree with unsupported worktreeConfig extension" 11 | ( 12 | set -e 13 | 14 | reponame="$(basename "$0" ".sh")-unsupported" 15 | mkdir "$reponame" 16 | cd "$reponame" 17 | git init 18 | 19 | set +e 20 | git lfs install --worktree 2>err.log 21 | res=$? 22 | set -e 23 | 24 | cat err.log 25 | grep -i "error" err.log 26 | grep -- "--worktree" err.log 27 | [ "0" != "$res" ] 28 | ) 29 | end_test 30 | -------------------------------------------------------------------------------- /git-lfs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "sync" 8 | "syscall" 9 | 10 | "github.com/git-lfs/git-lfs/v3/commands" 11 | "github.com/git-lfs/git-lfs/v3/tr" 12 | ) 13 | 14 | func main() { 15 | c := make(chan os.Signal) 16 | signal.Notify(c, os.Interrupt, os.Kill) 17 | 18 | var once sync.Once 19 | 20 | go func() { 21 | for { 22 | sig := <-c 23 | once.Do(commands.Cleanup) 24 | fmt.Fprintf(os.Stderr, "\n%s\n", tr.Tr.Get("Exiting because of %q signal.", sig)) 25 | 26 | exitCode := 1 27 | if sysSig, ok := sig.(syscall.Signal); ok { 28 | exitCode = int(sysSig) 29 | } 30 | os.Exit(exitCode + 128) 31 | } 32 | }() 33 | 34 | code := commands.Run() 35 | once.Do(commands.Cleanup) 36 | os.Exit(code) 37 | } 38 | -------------------------------------------------------------------------------- /docs/man/git-lfs-clean.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-clean(1) 2 | 3 | == NAME 4 | 5 | git-lfs-clean - Git clean filter that converts large files to pointers 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs clean` 10 | 11 | == DESCRIPTION 12 | 13 | Read the contents of a large file from standard input, and write a Git 14 | LFS pointer file for that file to standard output. 15 | 16 | Clean is typically run by Git's clean filter, configured by the 17 | repository's Git attributes. 18 | 19 | Clean is not part of the user-facing Git plumbing commands. To preview 20 | the pointer of a large file as it would be generated, see the 21 | git-lfs-pointer(1) command. 22 | 23 | == SEE ALSO 24 | 25 | git-lfs-install(1), git-lfs-push(1), git-lfs-pointer(1), 26 | gitattributes(5). 27 | 28 | Part of the git-lfs(1) suite. 29 | -------------------------------------------------------------------------------- /docs/man/git-lfs-logs.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-logs(1) 2 | 3 | == NAME 4 | 5 | git-lfs-logs - Show errors from the git-lfs command 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs logs` + 10 | `git lfs logs` + 11 | `git lfs logs clear` + 12 | `git lfs logs boomtown` 13 | 14 | == DESCRIPTION 15 | 16 | Display errors from the git-lfs command. Any time it crashes, the 17 | details are saved to ".git/lfs/logs". 18 | 19 | == COMMANDS 20 | 21 | `clear`:: 22 | Clears all of the existing logged errors. 23 | `boomtown`:: 24 | Triggers a dummy exception. 25 | 26 | == OPTIONS 27 | 28 | Without any options, `git lfs logs` simply shows the list of error logs. 29 | 30 | ``:: 31 | Shows the specified error log. Use "last" to show the most recent error. 32 | 33 | == SEE ALSO 34 | 35 | Part of the git-lfs(1) suite. 36 | -------------------------------------------------------------------------------- /lfsapi/body.go: -------------------------------------------------------------------------------- 1 | package lfsapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "strconv" 9 | ) 10 | 11 | type ReadSeekCloser interface { 12 | io.Seeker 13 | io.ReadCloser 14 | } 15 | 16 | func MarshalToRequest(req *http.Request, obj interface{}) error { 17 | by, err := json.Marshal(obj) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | clen := len(by) 23 | req.Header.Set("Content-Length", strconv.Itoa(clen)) 24 | req.ContentLength = int64(clen) 25 | req.Body = NewByteBody(by) 26 | return nil 27 | } 28 | 29 | func NewByteBody(by []byte) ReadSeekCloser { 30 | return &closingByteReader{Reader: bytes.NewReader(by)} 31 | } 32 | 33 | type closingByteReader struct { 34 | *bytes.Reader 35 | } 36 | 37 | func (r *closingByteReader) Close() error { 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /lfshttp/body.go: -------------------------------------------------------------------------------- 1 | package lfshttp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "strconv" 9 | ) 10 | 11 | type ReadSeekCloser interface { 12 | io.Seeker 13 | io.ReadCloser 14 | } 15 | 16 | func MarshalToRequest(req *http.Request, obj interface{}) error { 17 | by, err := json.Marshal(obj) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | clen := len(by) 23 | req.Header.Set("Content-Length", strconv.Itoa(clen)) 24 | req.ContentLength = int64(clen) 25 | req.Body = NewByteBody(by) 26 | return nil 27 | } 28 | 29 | func NewByteBody(by []byte) ReadSeekCloser { 30 | return &closingByteReader{Reader: bytes.NewReader(by)} 31 | } 32 | 33 | type closingByteReader struct { 34 | *bytes.Reader 35 | } 36 | 37 | func (r *closingByteReader) Close() error { 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /docs/man/git-lfs-standalone-file.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-standalone-file(1) 2 | 3 | == NAME 4 | 5 | git-lfs-standalone-file - Standalone transfer adapter for file URLs 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs standalone-file` 10 | 11 | == DESCRIPTION 12 | 13 | Provides a standalone transfer adapter for file URLs (local paths). 14 | 15 | By default, Git LFS requires the support of an HTTP server to implement 16 | the Git LFS protocol. However, this tool allows the use of URLs starting 17 | with `file:///` (that is, those representing local paths) in addition. 18 | Configuration is not necessary; Git LFS handles this internally. 19 | 20 | When invoked, this tool speaks JSON on input and output as a standalone 21 | transfer adapter. It is not intended for use by end users. 22 | 23 | == SEE ALSO 24 | 25 | Part of the git-lfs(1) suite. 26 | -------------------------------------------------------------------------------- /t/t-install-custom-hooks-path-unsupported.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | # These tests rely on behavior found in Git versions less than 2.9.0 to perform 6 | # themselves, specifically: 7 | # - lack of core.hooksPath support 8 | ensure_git_version_isnt $VERSION_HIGHER "2.9.0" 9 | 10 | begin_test "install with unsupported core.hooksPath" 11 | ( 12 | set -e 13 | 14 | repo_name="unsupported-custom-hooks-path" 15 | git init "$repo_name" 16 | cd "$repo_name" 17 | 18 | hooks_dir="custom_hooks_dir" 19 | mkdir -p "$hooks_dir" 20 | 21 | git config --local core.hooksPath "$hooks_dir" 22 | 23 | git lfs install 2>&1 | tee install.log 24 | grep "Updated Git hooks" install.log 25 | 26 | [ ! -e "$hooks_dir/pre-push" ] 27 | [ -e ".git/hooks/pre-push" ] 28 | ) 29 | end_test 30 | -------------------------------------------------------------------------------- /t/t-repo-format.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "repository format version" 6 | ( 7 | set -e 8 | 9 | reponame="lfs-repo-version" 10 | git init $reponame 11 | cd $reponame 12 | 13 | [ -z "$(git config --local lfs.repositoryFormatVersion)" ] 14 | 15 | git lfs track '*.dat' 16 | 17 | [ "$(git config --local lfs.repositoryFormatVersion)" = "0" ] 18 | 19 | git config --local lfs.repositoryFormatVersion 1 20 | 21 | git lfs track '*.bin' >output 2>&1 && exit 1 22 | cat output 23 | grep "Unknown repository format version: 1" output 24 | 25 | git config --local --unset lfs.repositoryFormatVersion 26 | # Verify that global settings are ignored. 27 | git config --global lfs.repositoryFormatVersion 1 28 | 29 | git lfs track '*.bin' 30 | ) 31 | end_test 32 | -------------------------------------------------------------------------------- /t/t-upload-redirect.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "redirect upload" 6 | ( 7 | set -e 8 | 9 | reponame="redirect-storage-upload" 10 | setup_remote_repo "$reponame" 11 | clone_repo "$reponame" redirect-repo-upload 12 | 13 | contents="redirect-storage-upload" 14 | oid="$(calc_oid "$contents")" 15 | printf "%s" "$contents" > a.dat 16 | 17 | git lfs track "*.dat" 18 | git add .gitattributes a.dat 19 | git commit -m "initial commit" 20 | 21 | GIT_TRACE=1 git push origin main 2>&1 | tee push.log 22 | if [ "0" -ne "${PIPESTATUS[0]}" ]; then 23 | echo >&2 "fatal: expected \`git push origin main\` to succeed ..." 24 | exit 1 25 | fi 26 | 27 | grep "api: redirect" push.log 28 | 29 | assert_server_object "$reponame" "$oid" 30 | ) 31 | end_test 32 | -------------------------------------------------------------------------------- /tq/errors.go: -------------------------------------------------------------------------------- 1 | package tq 2 | 3 | import "github.com/git-lfs/git-lfs/v3/tr" 4 | 5 | type MalformedObjectError struct { 6 | Name string 7 | Oid string 8 | 9 | missing bool 10 | } 11 | 12 | func newObjectMissingError(name, oid string) error { 13 | return &MalformedObjectError{Name: name, Oid: oid, missing: true} 14 | } 15 | 16 | func newCorruptObjectError(name, oid string) error { 17 | return &MalformedObjectError{Name: name, Oid: oid, missing: false} 18 | } 19 | 20 | func (e MalformedObjectError) Missing() bool { return e.missing } 21 | 22 | func (e MalformedObjectError) Corrupt() bool { return !e.Missing() } 23 | 24 | func (e MalformedObjectError) Error() string { 25 | if e.Corrupt() { 26 | return tr.Tr.Get("corrupt object: %s (%s)", e.Name, e.Oid) 27 | } 28 | return tr.Tr.Get("missing object: %s (%s)", e.Name, e.Oid) 29 | } 30 | -------------------------------------------------------------------------------- /lfs/gitfilter.go: -------------------------------------------------------------------------------- 1 | package lfs 2 | 3 | import ( 4 | "github.com/git-lfs/git-lfs/v3/config" 5 | "github.com/git-lfs/git-lfs/v3/fs" 6 | "github.com/git-lfs/git-lfs/v3/git" 7 | "github.com/jmhodges/clock" 8 | ) 9 | 10 | // GitFilter provides clean and smudge capabilities 11 | type GitFilter struct { 12 | cfg *config.Configuration 13 | fs *fs.Filesystem 14 | clk clock.Clock 15 | } 16 | 17 | // NewGitFilter initializes a new *GitFilter 18 | func NewGitFilter(cfg *config.Configuration) *GitFilter { 19 | return &GitFilter{cfg: cfg, fs: cfg.Filesystem(), clk: clock.New()} 20 | } 21 | 22 | func (f *GitFilter) ObjectPath(oid string) (string, error) { 23 | return f.fs.ObjectPath(oid) 24 | } 25 | 26 | func (f *GitFilter) RemoteRef() *git.Ref { 27 | return git.NewRefUpdate(f.cfg.Git, f.cfg.PushRemote(), f.cfg.CurrentRef(), nil).RemoteRef() 28 | } 29 | -------------------------------------------------------------------------------- /tools/time_tools.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // IsExpiredAtOrIn returns whether or not the result of calling TimeAtOrIn is 8 | // "expired" within "until" units of time from now. 9 | func IsExpiredAtOrIn(from time.Time, until time.Duration, at time.Time, in time.Duration) (time.Time, bool) { 10 | expiration := TimeAtOrIn(from, at, in) 11 | if expiration.IsZero() { 12 | return expiration, false 13 | } 14 | 15 | return expiration, expiration.Before(time.Now().Add(until)) 16 | } 17 | 18 | // TimeAtOrIn returns either "at", or the "in" duration added to the current 19 | // time. TimeAtOrIn prefers to add a duration rather than return the "at" 20 | // parameter. 21 | func TimeAtOrIn(from, at time.Time, in time.Duration) time.Time { 22 | if in == 0 { 23 | return at 24 | } 25 | return from.Add(in) 26 | } 27 | -------------------------------------------------------------------------------- /config/fetcher.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Fetcher provides an interface to get typed information out of a configuration 4 | // "source". These sources could be the OS environment, a .gitconfig, or even 5 | // just a `map`. 6 | type Fetcher interface { 7 | // Get returns the string value associated with a given key and a bool 8 | // determining if the key exists. 9 | // 10 | // If multiple entries match the given key, the first one will be 11 | // returned. 12 | Get(key string) (val string, ok bool) 13 | 14 | // GetAll returns the a set of string values associated with a given 15 | // key. If no entries matched the given key, an empty slice will be 16 | // returned instead. 17 | GetAll(key string) (vals []string) 18 | 19 | // All returns a copy of all the key/value pairs for the current 20 | // environment. 21 | All() map[string][]string 22 | } 23 | -------------------------------------------------------------------------------- /docs/man/git-lfs-post-commit.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-post-commit(1) 2 | 3 | == NAME 4 | 5 | git-lfs-post-commit - Git post-commit hook implementation 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs post-commit` 10 | 11 | == DESCRIPTION 12 | 13 | Responds to Git post-commit events. It makes sure that any files which 14 | are marked as lockable by `git lfs track` are read-only in the working 15 | copy, if not currently locked by the local user. 16 | 17 | Where the `git lfs post-merge` command, which has a similar purpose, 18 | must examine all files in the working copy, `git lfs post-commit` can 19 | limit itself checking only those files which have changed in `HEAD`. It 20 | primarily handles newly added lockable files which have not yet been 21 | made read-only. 22 | 23 | == SEE ALSO 24 | 25 | git-lfs-post-merge(1), git-lfs-track(1) 26 | 27 | Part of the git-lfs(1) suite. 28 | -------------------------------------------------------------------------------- /t/t-batch-unknown-oids.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "transfer queue rejects unknown OIDs" 6 | ( 7 | set -e 8 | 9 | reponame="unknown-oids" 10 | setup_remote_repo "$reponame" 11 | clone_repo "$reponame" "$reponame" 12 | 13 | git lfs track "*.dat" 14 | git add .gitattributes 15 | git commit -m "initial commit" 16 | 17 | contents="unknown-oid" 18 | printf "%s" "$contents" > a.dat 19 | 20 | git add a.dat 21 | git commit -m "add objects" 22 | 23 | set +e 24 | git push origin main 2>&1 | tee push.log 25 | res="${PIPESTATUS[0]}" 26 | set -e 27 | 28 | refute_server_object "$reponame" "$(calc_oid "$contents")" 29 | if [ "0" -eq "$res" ]; then 30 | echo "push successful?" 31 | exit 1 32 | fi 33 | 34 | grep "\[unknown-oid\] The server returned an unknown OID." push.log 35 | ) 36 | end_test 37 | -------------------------------------------------------------------------------- /t/t-cherry-pick-commits.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "cherry-pick two commits without lfs cache" 6 | ( 7 | set -e 8 | 9 | reponame="$(basename "$0" ".sh")-cherry-pick-commits" 10 | setup_remote_repo "$reponame" 11 | clone_repo "$reponame" cherrypickcommits 12 | 13 | git lfs track "*.dat" 14 | git add .gitattributes 15 | git commit -m "initial commit" 16 | 17 | git branch secondbranch 18 | 19 | echo "smudge a" > a.dat 20 | git add a.dat 21 | git commit -m "add a.dat" 22 | commit1=$(git log -n1 --format="%H") 23 | 24 | echo "smudge b" > b.dat 25 | git add b.dat 26 | git commit -m "add a.dat" 27 | commit2=$(git log -n1 --format="%H") 28 | 29 | git push origin main 30 | 31 | git checkout secondbranch 32 | rm -rf .git/lfs/objects 33 | 34 | git cherry-pick $commit1 $commit2 35 | ) 36 | end_test 37 | -------------------------------------------------------------------------------- /t/t-push-bad-dns.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | ensure_git_version_isnt $VERSION_LOWER "2.3.0" 6 | 7 | begin_test "push: upload to bad dns" 8 | ( 9 | set -e 10 | 11 | reponame="$(basename "$0" ".sh")-bad-dns" 12 | setup_remote_repo "$reponame" 13 | clone_repo "$reponame" "$reponame" 14 | 15 | git lfs track "*.dat" 16 | printf "hi" > good.dat 17 | git add .gitattributes good.dat 18 | git commit -m "welp" 19 | 20 | port="$(echo "http://127.0.0.1:63378" | cut -f 3 -d ":")" 21 | git config lfs.url "http://git-lfs-bad-dns:$port" 22 | 23 | set +e 24 | GIT_TERMINAL_PROMPT=0 git push origin main 2>&1 | tee push.log 25 | res="${PIPESTATUS[0]}" 26 | set -e 27 | 28 | refute_server_object "$reponame" "$(calc_oid "hi")" 29 | if [ "$res" = "0" ]; then 30 | cat push.log 31 | 32 | echo "push successful?" 33 | exit 1 34 | fi 35 | ) 36 | end_test 37 | -------------------------------------------------------------------------------- /locking/schemas/http-lock-list-response-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema", 3 | "title": "Git LFS HTTPS Lock List API Response", 4 | "type": "object", 5 | "properties": { 6 | "locks": { 7 | "type": "array", 8 | "items": { 9 | "type": "object", 10 | "properties": { 11 | "id": { 12 | "type": "string" 13 | }, 14 | "path": { 15 | "type": "string" 16 | }, 17 | "locked_at": { 18 | "type": "string" 19 | }, 20 | "owner": { 21 | "type": "object", 22 | "properties": { 23 | "name": { 24 | "type": "string" 25 | } 26 | } 27 | } 28 | } 29 | } 30 | }, 31 | "next_cursor": { 32 | "type": "string" 33 | } 34 | }, 35 | "required": ["locks"] 36 | } 37 | -------------------------------------------------------------------------------- /docs/man/git-lfs-update.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-update(1) 2 | 3 | == NAME 4 | 5 | git-lfs-update - Update Git hooks 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs update` [--manual | --force] 10 | 11 | == DESCRIPTION 12 | 13 | Updates the Git hooks used by Git LFS. Silently upgrades known hook 14 | contents. If you have your own custom hooks you may need to use one of 15 | the extended options below. 16 | 17 | == OPTIONS 18 | 19 | `-m`:: 20 | `--manual`:: 21 | Print instructions for manually updating your hooks to include git-lfs 22 | functionality. Use this option if `git lfs update` fails because of existing 23 | hooks and you want to retain their functionality. 24 | `-f`:: 25 | `--force`:: 26 | Forcibly overwrite any existing hooks with git-lfs hooks. Use this option if 27 | `git lfs update` fails because of existing hooks but you don't care about 28 | their current contents. 29 | 30 | == SEE ALSO 31 | 32 | Part of the git-lfs(1) suite. 33 | -------------------------------------------------------------------------------- /t/t-tempfile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "cleans only temp files and directories older than an hour" 6 | ( 7 | set -e 8 | 9 | reponame="$(basename "$0" ".sh")" 10 | git init "$reponame" 11 | cd "$reponame" 12 | 13 | git lfs track '*.bin' 14 | echo foo > abc.bin 15 | git add abc.bin 16 | git commit -m 'Add abc.bin' 17 | 18 | tmpdir=.git/lfs/tmp 19 | mkdir -p "$tmpdir" 20 | 21 | mkdir "$tmpdir/dir-to-preserve" 22 | touch "$tmpdir/to-preserve" 23 | touch "$tmpdir/dir-to-preserve/file" 24 | # git format-patch datestamp; arbitrary timestamp in the past. 25 | TZ=UTC touch -t 200109170000.00 "$tmpdir/to-destroy" 26 | TZ=UTC touch -t 200109170000.00 "$tmpdir/dir-to-preserve/file" 27 | 28 | git lfs ls-files >/dev/null 29 | 30 | [ -f "$tmpdir/to-preserve" ] 31 | [ -f "$tmpdir/dir-to-preserve/file" ] 32 | [ ! -f "$tmpdir/to-destroy" ] 33 | ) 34 | end_test 35 | -------------------------------------------------------------------------------- /tools/robustio_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package tools 5 | 6 | import ( 7 | "errors" 8 | "os" 9 | 10 | "github.com/avast/retry-go" 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | // isEphemeralError returns true if err may be resolved by waiting. 15 | func isEphemeralError(err error) bool { 16 | return errors.Is(err, windows.ERROR_SHARING_VIOLATION) 17 | } 18 | 19 | func RobustRename(oldpath, newpath string) error { 20 | return retry.Do( 21 | func() error { 22 | return os.Rename(oldpath, newpath) 23 | }, 24 | retry.RetryIf(isEphemeralError), 25 | retry.LastErrorOnly(true), 26 | ) 27 | } 28 | 29 | func RobustOpen(name string) (*os.File, error) { 30 | var result *os.File 31 | return result, retry.Do( 32 | func() error { 33 | f, err := os.Open(name) 34 | result = f 35 | return err 36 | }, 37 | retry.RetryIf(isEphemeralError), 38 | retry.LastErrorOnly(true), 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /tq/schemas/http-batch-request-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema", 3 | "title": "Git LFS HTTPS Batch API Request", 4 | "type": "object", 5 | "properties": { 6 | "transfers": { 7 | "type": "array", 8 | "items": { 9 | "type": "string" 10 | } 11 | }, 12 | "operation": { 13 | "type": "string" 14 | }, 15 | "objects": { 16 | "type": "array", 17 | "items": { 18 | "type": "object", 19 | "properties": { 20 | "oid": { 21 | "type": "string" 22 | }, 23 | "size": { 24 | "type": "number", 25 | "minimum": 0 26 | }, 27 | "authenticated": { 28 | "type": "boolean" 29 | } 30 | }, 31 | "required": ["oid", "size"], 32 | "additionalProperties": false 33 | } 34 | } 35 | }, 36 | "required": ["objects", "operation"] 37 | } 38 | -------------------------------------------------------------------------------- /script/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | prefix="/usr/local" 5 | 6 | if [ "${PREFIX:-}" != "" ] ; then 7 | prefix=${PREFIX:-} 8 | elif [ "${BOXEN_HOME:-}" != "" ] ; then 9 | prefix=${BOXEN_HOME:-} 10 | fi 11 | 12 | while [[ $# -gt 0 ]]; do 13 | case "$1" in 14 | --local) 15 | prefix="$HOME/.local" 16 | shift 17 | ;; 18 | *) 19 | echo "Unknown option: $1" 20 | exit 1 21 | ;; 22 | esac 23 | done 24 | 25 | # Check if the user has permission to install in the specified prefix 26 | if [ ! -w "$prefix" ]; then 27 | echo "Error: Insufficient permissions to install in $prefix. Try running with sudo or choose a different prefix.">&2 28 | exit 1 29 | fi 30 | 31 | mkdir -p "$prefix/bin" 32 | rm -rf "$prefix/bin/git-lfs*" 33 | 34 | pushd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null 35 | for g in git*; do 36 | install "$g" "$prefix/bin/$g" 37 | done 38 | popd > /dev/null 39 | 40 | PATH+=:"$prefix/bin" 41 | git lfs install 42 | -------------------------------------------------------------------------------- /t/t-completion.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "completion: bash script" 6 | ( 7 | set -e 8 | 9 | git lfs completion bash | cmp - "$COMPLETIONSDIR/git-lfs-completion.bash" 10 | ) 11 | end_test 12 | 13 | begin_test "completion: fish script" 14 | ( 15 | set -e 16 | 17 | git lfs completion fish | cmp - "$COMPLETIONSDIR/git-lfs-completion.fish" 18 | ) 19 | end_test 20 | 21 | begin_test "completion: zsh script" 22 | ( 23 | set -e 24 | 25 | git lfs completion zsh | cmp - "$COMPLETIONSDIR/git-lfs-completion.zsh" 26 | ) 27 | end_test 28 | 29 | begin_test "completion: missing shell argument" 30 | ( 31 | set -e 32 | 33 | git lfs completion 2>&1 | tee completion.log 34 | grep "accepts 1 arg" completion.log 35 | ) 36 | end_test 37 | 38 | begin_test "completion: invalid shell argument" 39 | ( 40 | set -e 41 | 42 | git lfs completion ksh 2>&1 | tee completion.log 43 | grep "invalid argument" completion.log 44 | ) 45 | end_test 46 | -------------------------------------------------------------------------------- /tools/util_test.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestCopyWithCallback(t *testing.T) { 13 | buf := bytes.NewBufferString("BOOYA") 14 | 15 | called := 0 16 | calledWritten := make([]int64, 0, 2) 17 | 18 | n, err := CopyWithCallback(io.Discard, buf, 5, func(total int64, written int64, current int) error { 19 | called += 1 20 | calledWritten = append(calledWritten, written) 21 | assert.Equal(t, 5, int(total)) 22 | return nil 23 | }) 24 | assert.Nil(t, err) 25 | assert.Equal(t, 5, int(n)) 26 | 27 | assert.Equal(t, 1, called) 28 | assert.Len(t, calledWritten, 1) 29 | assert.Equal(t, 5, int(calledWritten[0])) 30 | } 31 | 32 | func TestMethodExists(t *testing.T) { 33 | // testing following methods exist in all platform. 34 | _, _ = CheckCloneFileSupported(os.TempDir()) 35 | _, _ = CloneFile(io.Writer(nil), io.Reader(nil)) 36 | _, _ = CloneFileByPath("", "") 37 | } 38 | -------------------------------------------------------------------------------- /locking/schemas/http-lock-create-response-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema", 3 | "title": "Git LFS HTTPS Lock Creation API Response", 4 | "type": "object", 5 | "properties": { 6 | "lock": { 7 | "type": "object", 8 | "properties": { 9 | "id": { 10 | "type": "string" 11 | }, 12 | "path": { 13 | "type": "string" 14 | }, 15 | "locked_at": { 16 | "type": "string" 17 | }, 18 | "owner": { 19 | "type": "object", 20 | "properties": { 21 | "name": { 22 | "type": "string" 23 | } 24 | } 25 | } 26 | }, 27 | "required": ["id", "path", "locked_at"] 28 | }, 29 | "message": { 30 | "type": "string" 31 | }, 32 | "request_id": { 33 | "type": "string" 34 | }, 35 | "documentation_url": { 36 | "type": "string" 37 | } 38 | }, 39 | "required": ["lock"] 40 | } 41 | -------------------------------------------------------------------------------- /creds/access.go: -------------------------------------------------------------------------------- 1 | package creds 2 | 3 | type AccessMode string 4 | 5 | const ( 6 | NoneAccess AccessMode = "none" 7 | BasicAccess AccessMode = "basic" 8 | PrivateAccess AccessMode = "private" 9 | NegotiateAccess AccessMode = "negotiate" 10 | EmptyAccess AccessMode = "" 11 | ) 12 | 13 | type Access struct { 14 | mode AccessMode 15 | url string 16 | } 17 | 18 | func NewAccess(mode AccessMode, url string) Access { 19 | return Access{url: url, mode: mode} 20 | } 21 | 22 | // Returns a copy of an AccessMode with the mode upgraded to newMode 23 | func (a *Access) Upgrade(newMode AccessMode) Access { 24 | return Access{url: a.url, mode: newMode} 25 | } 26 | 27 | func (a *Access) Mode() AccessMode { 28 | return a.mode 29 | } 30 | 31 | func (a *Access) URL() string { 32 | return a.url 33 | } 34 | 35 | // AllAccessModes returns all access modes in the order they should be tried. 36 | func AllAccessModes() []AccessMode { 37 | return []AccessMode{ 38 | NoneAccess, 39 | NegotiateAccess, 40 | BasicAccess, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/api/README.md: -------------------------------------------------------------------------------- 1 | # Git LFS API 2 | 3 | The Git LFS client uses an HTTPS server to coordinate fetching and storing 4 | large binary objects separately from a Git server. The basic process the client 5 | goes through looks like this: 6 | 7 | 1. [Discover the LFS Server to use](./server-discovery.md). 8 | 2. [Apply Authentication](./authentication.md). 9 | 3. Make the request. See the Batch and File Locking API sections. 10 | 11 | ## Batch API 12 | 13 | The Batch API is used to request the ability to transfer LFS objects with the 14 | LFS server. 15 | 16 | API Specification: 17 | * [Batch API](./batch.md) 18 | 19 | Current transfer adapters include: 20 | * [Basic](./basic-transfers.md) 21 | 22 | Experimental transfer adapters include: 23 | * Tus.io (upload only) 24 | * [Custom](../custom-transfers.md) 25 | 26 | ## File Locking API 27 | 28 | The File Locking API is used to create, list, and delete locks, as well as 29 | verify that locks are respected in Git pushes. 30 | 31 | API Specification: 32 | * [File Locking API](./locking.md) 33 | -------------------------------------------------------------------------------- /tools/channels.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import "github.com/git-lfs/git-lfs/v3/errors" 4 | 5 | // Interface for all types of wrapper around a channel of results and an error channel 6 | // Implementors will expose a type-specific channel for results 7 | // Call the Wait() function after processing the results channel to catch any errors 8 | // that occurred during the async processing 9 | type ChannelWrapper interface { 10 | // Call this after processing results channel to check for async errors 11 | Wait() error 12 | } 13 | 14 | // Base implementation of channel wrapper to just deal with errors 15 | type BaseChannelWrapper struct { 16 | errorChan <-chan error 17 | } 18 | 19 | func (w *BaseChannelWrapper) Wait() error { 20 | var multiErr error 21 | for err := range w.errorChan { 22 | // Combine in case multiple errors 23 | multiErr = errors.Join(multiErr, err) 24 | } 25 | 26 | return multiErr 27 | } 28 | 29 | func NewBaseChannelWrapper(errChan <-chan error) *BaseChannelWrapper { 30 | return &BaseChannelWrapper{errorChan: errChan} 31 | } 32 | -------------------------------------------------------------------------------- /errors/types_test.go: -------------------------------------------------------------------------------- 1 | package errors_test 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "github.com/git-lfs/git-lfs/v3/errors" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type TemporaryError struct { 12 | } 13 | 14 | func (e TemporaryError) Error() string { 15 | return "" 16 | } 17 | 18 | func (e TemporaryError) Temporary() bool { 19 | return true 20 | } 21 | 22 | type TimeoutError struct { 23 | } 24 | 25 | func (e TimeoutError) Error() string { 26 | return "" 27 | } 28 | 29 | func (e TimeoutError) Timeout() bool { 30 | return true 31 | } 32 | 33 | func TestCanRetryOnTemporaryError(t *testing.T) { 34 | err := &url.Error{Err: TemporaryError{}} 35 | assert.True(t, errors.IsRetriableError(err)) 36 | } 37 | 38 | func TestCanRetryOnTimeoutError(t *testing.T) { 39 | err := &url.Error{Err: TimeoutError{}} 40 | assert.True(t, errors.IsRetriableError(err)) 41 | } 42 | 43 | func TestCannotRetryOnGenericUrlError(t *testing.T) { 44 | err := &url.Error{Err: errors.New("")} 45 | assert.False(t, errors.IsRetriableError(err)) 46 | } 47 | -------------------------------------------------------------------------------- /tasklog/waiting_task.go: -------------------------------------------------------------------------------- 1 | package tasklog 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // WaitingTask represents a task for which the total number of items to do work 9 | // is on is unknown. 10 | type WaitingTask struct { 11 | // ch is used to transmit task updates. 12 | ch chan *Update 13 | } 14 | 15 | // NewWaitingTask returns a new *WaitingTask. 16 | func NewWaitingTask(msg string) *WaitingTask { 17 | ch := make(chan *Update, 1) 18 | ch <- &Update{ 19 | S: fmt.Sprintf("%s: ...", msg), 20 | At: time.Now(), 21 | } 22 | 23 | return &WaitingTask{ch: ch} 24 | } 25 | 26 | // Complete marks the task as completed. 27 | func (w *WaitingTask) Complete() { 28 | close(w.ch) 29 | } 30 | 31 | // Updates implements the Task.Updates function and returns a channel of updates 32 | // to log to the sink. 33 | func (w *WaitingTask) Updates() <-chan *Update { 34 | return w.ch 35 | } 36 | 37 | // Throttled implements Task.Throttled and returns true, indicating that this 38 | // task is Throttled. 39 | func (w *WaitingTask) Throttled() bool { return true } 40 | -------------------------------------------------------------------------------- /git/filter_process_status.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import "github.com/git-lfs/git-lfs/v3/tr" 4 | 5 | // FilterProcessStatus is a constant type representing the various valid 6 | // responses for `status=` in the Git filtering process protocol. 7 | type FilterProcessStatus uint8 8 | 9 | const ( 10 | // StatusSuccess is a valid response when a successful event has 11 | // occurred. 12 | StatusSuccess FilterProcessStatus = iota + 1 13 | // StatusDelay is a valid response when a delay has occurred. 14 | StatusDelay 15 | // StatusError is a valid response when an error has occurred. 16 | StatusError 17 | ) 18 | 19 | // String implements fmt.Stringer by returning a protocol-compliant 20 | // representation of the receiving status, or panic()-ing if the Status is 21 | // unknown. 22 | func (s FilterProcessStatus) String() string { 23 | switch s { 24 | case StatusSuccess: 25 | return "success" 26 | case StatusDelay: 27 | return "delayed" 28 | case StatusError: 29 | return "error" 30 | } 31 | 32 | panic(tr.Tr.Get("unknown FilterProcessStatus '%d'", s)) 33 | } 34 | -------------------------------------------------------------------------------- /config/extension.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "sort" 6 | 7 | "github.com/git-lfs/git-lfs/v3/tr" 8 | ) 9 | 10 | // An Extension describes how to manipulate files during smudge and clean. 11 | // Extensions are parsed from the Git config. 12 | type Extension struct { 13 | Name string 14 | Clean string 15 | Smudge string 16 | Priority int 17 | } 18 | 19 | // SortExtensions sorts a map of extensions in ascending order by Priority 20 | func SortExtensions(m map[string]Extension) ([]Extension, error) { 21 | pMap := make(map[int]Extension) 22 | priorities := make([]int, 0, len(m)) 23 | for n, ext := range m { 24 | p := ext.Priority 25 | if _, exist := pMap[p]; exist { 26 | err := errors.New(tr.Tr.Get("duplicate priority %d on %s", p, n)) 27 | return nil, err 28 | } 29 | pMap[p] = ext 30 | priorities = append(priorities, p) 31 | } 32 | 33 | sort.Ints(priorities) 34 | 35 | result := make([]Extension, len(priorities)) 36 | for i, p := range priorities { 37 | result[i] = pMap[p] 38 | } 39 | 40 | return result, nil 41 | } 42 | -------------------------------------------------------------------------------- /docs/man/git-lfs-dedup.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-dedup(1) 2 | 3 | == NAME 4 | 5 | git-lfs-dedup - Deduplicate Git LFS files 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs dedup` 10 | 11 | == DESCRIPTION 12 | 13 | Deduplicates storage by re-creating working tree files as clones of the 14 | files in the Git LFS storage directory using the operating system's 15 | copy-on-write file creation functionality. 16 | 17 | If the operating system or file system don't support copy-on-write file 18 | creation, this command exits unsuccessfully. 19 | 20 | This command will also exit without success if any Git LFS extensions 21 | are configured, as these will typically be used to alter the file 22 | contents before they are written to the Git LFS storage directory, and 23 | therefore the working tree files should not be copy-on-write clones of 24 | the LFS object files. 25 | 26 | == OPTIONS 27 | 28 | `-t`:: 29 | `--test`:: 30 | Checks whether the current operating system and file system support 31 | copy-on-write file creation, without actually cloning any Git LFS files. 32 | 33 | == SEE ALSO 34 | 35 | Part of the git-lfs(1) suite. 36 | -------------------------------------------------------------------------------- /t/t-filter-branch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "filter-branch (git-lfs/git-lfs#1773)" 6 | ( 7 | set -e 8 | 9 | reponame="filter-branch" 10 | setup_remote_repo "$reponame" 11 | clone_repo "$reponame" "$reponame" 12 | 13 | contents_a="contents (a)" 14 | printf "%s" "$contents_a" > a.dat 15 | git add a.dat 16 | git commit -m "add a.dat" 17 | 18 | contents_b="contents (b)" 19 | printf "%s" "$contents_b" > b.dat 20 | git add b.dat 21 | git commit -m "add b.dat" 22 | 23 | contents_c="contents (c)" 24 | printf "%s" "$contents_c" > c.dat 25 | git add c.dat 26 | git commit -m "add c.dat" 27 | 28 | git filter-branch -f --prune-empty \ 29 | --tree-filter ' 30 | echo >&2 "---" 31 | git rm --cached -r -q . 32 | git lfs track "*.dat" 33 | git add . 34 | ' --tag-name-filter cat -- --all 35 | 36 | 37 | assert_pointer "main" "a.dat" "$(calc_oid "$contents_a")" 12 38 | assert_pointer "main" "b.dat" "$(calc_oid "$contents_b")" 12 39 | assert_pointer "main" "c.dat" "$(calc_oid "$contents_c")" 12 40 | ) 41 | end_test 42 | -------------------------------------------------------------------------------- /commands/command_ext.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/git-lfs/git-lfs/v3/config" 7 | "github.com/git-lfs/git-lfs/v3/tr" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func extCommand(cmd *cobra.Command, args []string) { 12 | printAllExts() 13 | } 14 | 15 | func extListCommand(cmd *cobra.Command, args []string) { 16 | n := len(args) 17 | if n == 0 { 18 | printAllExts() 19 | return 20 | } 21 | 22 | for _, key := range args { 23 | ext := cfg.Extensions()[key] 24 | printExt(ext) 25 | } 26 | } 27 | 28 | func printAllExts() { 29 | extensions, err := cfg.SortedExtensions() 30 | if err != nil { 31 | fmt.Println(err) 32 | return 33 | } 34 | for _, ext := range extensions { 35 | printExt(ext) 36 | } 37 | } 38 | 39 | func printExt(ext config.Extension) { 40 | Print(tr.Tr.Get("Extension: %s", ext.Name)) 41 | Print(` clean = %s 42 | smudge = %s 43 | priority = %d`, ext.Clean, ext.Smudge, ext.Priority) 44 | } 45 | 46 | func init() { 47 | RegisterCommand("ext", extCommand, func(cmd *cobra.Command) { 48 | cmd.AddCommand(NewCommand("list", extListCommand)) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /lfshttp/retries.go: -------------------------------------------------------------------------------- 1 | package lfshttp 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // ckey is a type that wraps a string for package-unique context.Context keys. 9 | type ckey string 10 | 11 | const ( 12 | // contextKeyRetries is a context.Context key for storing the desired 13 | // number of retries for a given request. 14 | contextKeyRetries ckey = "retries" 15 | 16 | // defaultRequestRetries is the default number of retries to perform on 17 | // a given HTTP request. 18 | defaultRequestRetries = 0 19 | ) 20 | 21 | // WithRetries stores the desired number of retries "n" on the given 22 | // http.Request, and causes it to be retried "n" times in the case of a non-nil 23 | // network related error. 24 | func WithRetries(req *http.Request, n int) *http.Request { 25 | ctx := req.Context() 26 | ctx = context.WithValue(ctx, contextKeyRetries, n) 27 | 28 | return req.WithContext(ctx) 29 | } 30 | 31 | // Retries returns the number of retries requested for a given http.Request. 32 | func Retries(req *http.Request) (int, bool) { 33 | n, ok := req.Context().Value(contextKeyRetries).(int) 34 | 35 | return n, ok 36 | } 37 | -------------------------------------------------------------------------------- /config/map_fetcher.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // mapFetcher provides an implementation of the Fetcher interface by wrapping 4 | // the `map[string]string` type. 5 | type mapFetcher map[string][]string 6 | 7 | func UniqMapFetcher(m map[string]string) Fetcher { 8 | multi := make(map[string][]string, len(m)) 9 | for k, v := range m { 10 | multi[k] = []string{v} 11 | } 12 | 13 | return MapFetcher(multi) 14 | } 15 | 16 | func MapFetcher(m map[string][]string) Fetcher { 17 | return mapFetcher(m) 18 | } 19 | 20 | // Get implements the func `Fetcher.Get`. 21 | func (m mapFetcher) Get(key string) (val string, ok bool) { 22 | all := m.GetAll(key) 23 | 24 | if len(all) == 0 { 25 | return "", false 26 | } 27 | return all[len(all)-1], true 28 | } 29 | 30 | // Get implements the func `Fetcher.GetAll`. 31 | func (m mapFetcher) GetAll(key string) []string { 32 | return m[key] 33 | } 34 | 35 | func (m mapFetcher) All() map[string][]string { 36 | newmap := make(map[string][]string) 37 | for key, values := range m { 38 | for _, value := range values { 39 | newmap[key] = append(newmap[key], value) 40 | } 41 | } 42 | return newmap 43 | } 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other issue 3 | about: Ask a question, request a feature, or report something that's not a bug 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the issue** 11 | A clear and concise description of why you wrote in today. 12 | 13 | **System environment** 14 | The version of your operating system, plus any relevant information about platform or configuration (e.g., container or CI usage, Cygwin, WSL, or non-Basic authentication). If relevant, include the output of `git config -l` as a code block. 15 | 16 | **Output of `git lfs env`** 17 | The output of running `git lfs env` as a code block. 18 | 19 | **Additional context** 20 | Any other relevant context about the problem here. 21 | 22 | 29 | -------------------------------------------------------------------------------- /fs/fs_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDecodeNone(t *testing.T) { 11 | evaluate(t, "A:\\some\\regular\\windows\\path", "A:\\some\\regular\\windows\\path") 12 | } 13 | 14 | func TestDecodeSingle(t *testing.T) { 15 | evaluate(t, "A:\\bl\\303\\204\\file.txt", "A:\\blÄ\\file.txt") 16 | } 17 | 18 | func TestDecodeMultiple(t *testing.T) { 19 | evaluate(t, "A:\\fo\\130\\file\\303\\261.txt", "A:\\fo\130\\file\303\261.txt") 20 | } 21 | 22 | func evaluate(t *testing.T, input string, expected string) { 23 | 24 | fs := Filesystem{} 25 | output := fs.DecodePathname(input) 26 | 27 | if output != expected { 28 | t.Errorf("Expecting same path, got: %s, want: %s.", output, expected) 29 | } 30 | 31 | } 32 | 33 | func TestRepositoryPermissions(t *testing.T) { 34 | m := map[os.FileMode]os.FileMode{ 35 | 0777: 0666, 36 | 0755: 0644, 37 | 0700: 0600, 38 | } 39 | for k, v := range m { 40 | fs := Filesystem{repoPerms: v} 41 | assert.Equal(t, k, fs.RepositoryPermissions(true)) 42 | assert.Equal(t, v, fs.RepositoryPermissions(false)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/man/git-lfs-unlock.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-unlock(1) 2 | 3 | == NAME 4 | 5 | git-lfs-unlock - Remove "locked" setting for a file on the Git LFS server 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs unlock` [] 10 | 11 | == DESCRIPTION 12 | 13 | Removes the given file path as "locked" on the Git LFS server. Files 14 | must exist and have a clean git status before they can be unlocked. The 15 | `--force` flag will skip these checks. 16 | 17 | == OPTIONS 18 | 19 | `-r `:: 20 | `--remote=`:: 21 | Specify the Git LFS server to use. Ignored if the `lfs.url` config key is 22 | set. 23 | `-f`:: 24 | `--force`:: 25 | Tells the server to remove the lock, even if it's owned by another user. 26 | `-i `:: 27 | `--id=`:: 28 | Specifies a lock by its ID instead of path. 29 | `-j`:: 30 | `--json`:: 31 | Writes lock info as JSON to STDOUT if the command exits successfully. Intended 32 | for interoperation with external tools. If the command returns with a non-zero 33 | exit code, plain text messages will be sent to STDERR. 34 | 35 | == SEE ALSO 36 | 37 | git-lfs-lock(1), git-lfs-locks(1). 38 | 39 | Part of the git-lfs(1) suite. 40 | -------------------------------------------------------------------------------- /docs/man/git-lfs-lock.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-lock(1) 2 | 3 | == NAME 4 | 5 | git-lfs-lock - Set a file as "locked" on the Git LFS server 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs lock` [options] 10 | 11 | == DESCRIPTION 12 | 13 | Sets the given file path as "locked" against the Git LFS server, with 14 | the intention of blocking attempts by other users to update the given 15 | path. Locking a file requires the file to exist in the working copy. 16 | 17 | Once locked, LFS will verify that Git pushes do not modify files locked 18 | by other users. See the description of the `lfs..locksverify` 19 | config key in git-lfs-config(5) for details. 20 | 21 | == OPTIONS 22 | 23 | `-r `:: 24 | `--remote=`:: 25 | Specify the Git LFS server to use. Ignored if the `lfs.url` config key is 26 | set. 27 | `-j`:: 28 | `--json`:: 29 | Writes lock info as JSON to STDOUT if the command exits successfully. Intended 30 | for interoperation with external tools. If the command returns with a non-zero 31 | exit code, plain text messages will be sent to STDERR. 32 | 33 | == SEE ALSO 34 | 35 | git-lfs-unlock(1), git-lfs-locks(1). 36 | 37 | Part of the git-lfs(1) suite. 38 | -------------------------------------------------------------------------------- /tq/manifest_test.go: -------------------------------------------------------------------------------- 1 | package tq 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/git-lfs/git-lfs/v3/lfsapi" 7 | "github.com/git-lfs/git-lfs/v3/lfshttp" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestManifestIsConfigurable(t *testing.T) { 13 | cli, err := lfsapi.NewClient(lfshttp.NewContext(nil, nil, map[string]string{ 14 | "lfs.transfer.maxretries": "3", 15 | })) 16 | require.Nil(t, err) 17 | 18 | m := NewManifest(nil, cli, "", "") 19 | assert.Equal(t, 3, m.MaxRetries()) 20 | } 21 | 22 | func TestManifestClampsValidValues(t *testing.T) { 23 | cli, err := lfsapi.NewClient(lfshttp.NewContext(nil, nil, map[string]string{ 24 | "lfs.transfer.maxretries": "-1", 25 | })) 26 | require.Nil(t, err) 27 | 28 | m := NewManifest(nil, cli, "", "") 29 | assert.Equal(t, 8, m.MaxRetries()) 30 | } 31 | 32 | func TestManifestIgnoresNonInts(t *testing.T) { 33 | cli, err := lfsapi.NewClient(lfshttp.NewContext(nil, nil, map[string]string{ 34 | "lfs.transfer.maxretries": "not_an_int", 35 | })) 36 | require.Nil(t, err) 37 | 38 | m := NewManifest(nil, cli, "", "") 39 | assert.Equal(t, 8, m.MaxRetries()) 40 | } 41 | -------------------------------------------------------------------------------- /docs/man/git-lfs-pre-push.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-pre-push(1) 2 | 3 | == NAME 4 | 5 | git-lfs-pre-push - Git pre-push hook implementation 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs pre-push` [remoteurl] 10 | 11 | == DESCRIPTION 12 | 13 | Responds to Git pre-hook events. It reads the range of commits from 14 | STDIN, in the following format: 15 | 16 | .... 17 | SP SP SP \n 18 | .... 19 | 20 | It also takes the remote name and URL as arguments. 21 | 22 | If any of those Git objects are associated with Git LFS objects, those 23 | objects will be pushed to the Git LFS API. 24 | 25 | In the case of pushing a new branch, the list of Git objects will be all 26 | of the Git objects in this branch. 27 | 28 | In the case of deleting a branch, no attempts to push Git LFS objects 29 | will be made. 30 | 31 | == OPTIONS 32 | 33 | `-d`:: 34 | `--dry-run`:: 35 | Print the files that would be pushed, without actually pushing them. 36 | `GIT_LFS_SKIP_PUSH`:: 37 | Do nothing on pre-push. For more information, see git-lfs-config(5). 38 | 39 | == SEE ALSO 40 | 41 | git-lfs-clean(1), git-lfs-push(1). 42 | 43 | Part of the git-lfs(1) suite. 44 | -------------------------------------------------------------------------------- /locking/schemas/http-lock-verify-response-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema", 3 | "title": "Git LFS HTTPS Lock Verify API Response", 4 | "type": "object", 5 | 6 | "definitions": { 7 | "lock": { 8 | "type": "object", 9 | "properties": { 10 | "id": { 11 | "type": "string" 12 | }, 13 | "path": { 14 | "type": "string" 15 | }, 16 | "locked_at": { 17 | "type": "string" 18 | }, 19 | "owner": { 20 | "type": "object", 21 | "properties": { 22 | "name": { 23 | "type": "string" 24 | } 25 | } 26 | } 27 | }, 28 | "required": ["id", "path"] 29 | } 30 | }, 31 | 32 | "properties": { 33 | "ours": { 34 | "type": "array", 35 | "items": { 36 | "$ref": "#/definitions/lock" 37 | } 38 | }, 39 | "theirs": { 40 | "type": "array", 41 | "items": { 42 | "$ref": "#/definitions/lock" 43 | } 44 | }, 45 | "next_cursor": { 46 | "type": "string" 47 | } 48 | }, 49 | "required": ["ours", "theirs"] 50 | } 51 | -------------------------------------------------------------------------------- /docs/man/git-lfs-status.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-status(1) 2 | 3 | == NAME 4 | 5 | git-lfs-status - Show the status of Git LFS files in the working tree 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs status` [] 10 | 11 | == DESCRIPTION 12 | 13 | Display paths of Git LFS objects that 14 | 15 | * have not been pushed to the Git LFS server. These are large files that 16 | would be uploaded by `git push`. 17 | * have differences between the index file and the current HEAD commit. 18 | These are large files that would be committed by `git commit`. 19 | * have differences between the working tree and the index file. These 20 | are files that could be staged using `git add`. 21 | 22 | This command must be run in a non-bare repository. 23 | 24 | == OPTIONS 25 | 26 | `-p`:: 27 | `--porcelain`:: 28 | Give the output in an easy-to-parse format for scripts. 29 | `-j`:: 30 | `--json`:: 31 | Writes Git LFS file status information as JSON to STDOUT, if the command 32 | exits successfully. Intended for interoperation with external tools. 33 | If `--porcelain` is also provided, that option takes precedence. 34 | 35 | == SEE ALSO 36 | 37 | git-lfs-ls-files(1). 38 | 39 | Part of the git-lfs(1) suite. 40 | -------------------------------------------------------------------------------- /config/git_fetcher_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGetCanonicalization(t *testing.T) { 10 | vals := map[string][]string{ 11 | "user.name": []string{"Pat Doe"}, 12 | "branch.MixedCase.pushremote": []string{"Somewhere"}, 13 | "http.https://example.com/BIG-TEXT.git.extraheader": []string{"X-Foo: Bar"}, 14 | } 15 | 16 | fetcher := GitFetcher{vals: vals} 17 | assert.Equal(t, []string{"Somewhere"}, fetcher.GetAll("bRanch.MixedCase.pushRemote")) 18 | assert.Equal(t, []string{"Somewhere"}, fetcher.GetAll("branch.MixedCase.pushremote")) 19 | assert.Equal(t, []string(nil), fetcher.GetAll("branch.mixedcase.pushremote")) 20 | assert.Equal(t, []string{"Pat Doe"}, fetcher.GetAll("user.name")) 21 | assert.Equal(t, []string{"Pat Doe"}, fetcher.GetAll("User.Name")) 22 | assert.Equal(t, []string{"X-Foo: Bar"}, fetcher.GetAll("http.https://example.com/BIG-TEXT.git.extraheader")) 23 | assert.Equal(t, []string{"X-Foo: Bar"}, fetcher.GetAll("http.https://example.com/BIG-TEXT.git.extraHeader")) 24 | assert.Equal(t, []string(nil), fetcher.GetAll("http.https://example.com/big-text.git.extraHeader")) 25 | } 26 | -------------------------------------------------------------------------------- /tools/cygwin_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package tools 5 | 6 | import ( 7 | "bytes" 8 | 9 | "github.com/git-lfs/git-lfs/v3/subprocess" 10 | "github.com/git-lfs/git-lfs/v3/tr" 11 | ) 12 | 13 | type cygwinSupport byte 14 | 15 | const ( 16 | cygwinStateUnknown cygwinSupport = iota 17 | cygwinStateEnabled 18 | cygwinStateDisabled 19 | ) 20 | 21 | func (c cygwinSupport) Enabled() bool { 22 | switch c { 23 | case cygwinStateEnabled: 24 | return true 25 | case cygwinStateDisabled: 26 | return false 27 | default: 28 | panic(tr.Tr.Get("unknown enabled state for %v", c)) 29 | } 30 | } 31 | 32 | var ( 33 | cygwinState cygwinSupport 34 | ) 35 | 36 | func isCygwin() bool { 37 | if cygwinState != cygwinStateUnknown { 38 | return cygwinState.Enabled() 39 | } 40 | 41 | cmd, err := subprocess.ExecCommand("uname") 42 | if err != nil { 43 | return false 44 | } 45 | out, err := cmd.Output() 46 | if err != nil { 47 | return false 48 | } 49 | 50 | if bytes.Contains(out, []byte("CYGWIN")) || bytes.Contains(out, []byte("MSYS")) { 51 | cygwinState = cygwinStateEnabled 52 | } else { 53 | cygwinState = cygwinStateDisabled 54 | } 55 | 56 | return cygwinState.Enabled() 57 | } 58 | -------------------------------------------------------------------------------- /t/t-mergetool.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "mergetool works with large files" 6 | ( 7 | set -e 8 | 9 | reponame="mergetool-works-with-large-files" 10 | git init "$reponame" 11 | cd "$reponame" 12 | 13 | git lfs track "*.dat" 14 | printf "base" > conflict.dat 15 | git add .gitattributes conflict.dat 16 | git commit -m "initial commit" 17 | 18 | git checkout -b conflict 19 | printf "b" > conflict.dat 20 | git add conflict.dat 21 | git commit -m "conflict.dat: b" 22 | 23 | git checkout main 24 | 25 | printf "a" > conflict.dat 26 | git add conflict.dat 27 | git commit -m "conflict.dat: a" 28 | 29 | set +e 30 | git merge conflict 31 | set -e 32 | 33 | git config mergetool.inspect.cmd ' 34 | for i in BASE LOCAL REMOTE; do 35 | echo "\$$i=$(eval "cat \"\$$i\"")"; 36 | done; 37 | exit 1 38 | ' 39 | git config mergetool.inspect.trustExitCode true 40 | 41 | yes | git mergetool \ 42 | --no-prompt \ 43 | --tool=inspect \ 44 | -- conflict.dat 2>&1 \ 45 | | tee mergetool.log 46 | 47 | grep "\$BASE=base" mergetool.log 48 | grep "\$LOCAL=a" mergetool.log 49 | grep "\$REMOTE=b" mergetool.log 50 | ) 51 | end_test 52 | -------------------------------------------------------------------------------- /t/t-track-attrs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | ensure_git_version_isnt $VERSION_LOWER "2.1.0" 6 | 7 | begin_test "track (--no-modify-attrs)" 8 | ( 9 | set -e 10 | 11 | reponame="track-no-modify-attrs" 12 | git init "$reponame" 13 | cd "$reponame" 14 | 15 | echo "contents" > a.dat 16 | git add a.dat 17 | 18 | # Git assumes that identical results from `stat(1)` between the index and 19 | # working copy are stat dirty. To prevent this, wait at least one second to 20 | # yield different `stat(1)` results. 21 | sleep 1 22 | 23 | git commit -m "add a.dat" 24 | 25 | echo "*.dat filter=lfs diff=lfs merge=lfs -text" > .gitattributes 26 | 27 | git add .gitattributes 28 | git commit -m "asdf" 29 | 30 | [ -z "$(git status --porcelain)" ] 31 | 32 | git lfs track --no-modify-attrs "*.dat" 33 | 34 | [ " M a.dat" = "$(git status --porcelain)" ] 35 | ) 36 | end_test 37 | 38 | begin_test "track (--dry-run)" 39 | ( 40 | set -e 41 | 42 | reponame="track-dry-run" 43 | git init "$reponame" 44 | cd "$reponame" 45 | 46 | git lfs track --dry-run "*.dat" 47 | 48 | echo "contents" > a.dat 49 | git add a.dat 50 | 51 | git commit -m "add a.dat" 52 | refute_pointer "main" "a.dat" 53 | ) 54 | end_test 55 | -------------------------------------------------------------------------------- /tools/filetools_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package tools 5 | 6 | import ( 7 | "golang.org/x/sys/windows" 8 | ) 9 | 10 | func openSymlink(path string) (windows.Handle, error) { 11 | p, err := windows.UTF16PtrFromString(path) 12 | if err != nil { 13 | return 0, err 14 | } 15 | 16 | attrs := uint32(windows.FILE_FLAG_BACKUP_SEMANTICS) 17 | h, err := windows.CreateFile(p, 0, 0, nil, windows.OPEN_EXISTING, attrs, 0) 18 | if err != nil { 19 | return 0, err 20 | } 21 | 22 | return h, nil 23 | } 24 | 25 | func CanonicalizeSystemPath(path string) (string, error) { 26 | h, err := openSymlink(path) 27 | if err != nil { 28 | return "", err 29 | } 30 | defer windows.CloseHandle(h) 31 | 32 | buf := make([]uint16, 100) 33 | for { 34 | n, err := windows.GetFinalPathNameByHandle(h, &buf[0], uint32(len(buf)), 0) 35 | if err != nil { 36 | return "", err 37 | } 38 | if n < uint32(len(buf)) { 39 | break 40 | } 41 | buf = make([]uint16, n) 42 | } 43 | 44 | s := windows.UTF16ToString(buf) 45 | if len(s) > 4 && s[:4] == `\\?\` { 46 | s = s[4:] 47 | if len(s) > 3 && s[:3] == `UNC` { 48 | // return path like \\server\share\... 49 | return `\` + s[3:], nil 50 | } 51 | return s, nil 52 | } 53 | return s, nil 54 | } 55 | -------------------------------------------------------------------------------- /commands/uploader_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type LockingSupportTestCase struct { 10 | Given string 11 | ExpectedToMatch bool 12 | } 13 | 14 | func (l *LockingSupportTestCase) Assert(t *testing.T) { 15 | assert.Equal(t, l.ExpectedToMatch, supportsLockingAPI(l.Given)) 16 | } 17 | 18 | func TestSupportedLockingHosts(t *testing.T) { 19 | for desc, c := range map[string]*LockingSupportTestCase{ 20 | "https with path prefix": {"https://github.com/ttaylorr/dotfiles.git/info/lfs", true}, 21 | "https with root": {"https://github.com/ttaylorr/dotfiles", true}, 22 | "http with path prefix": {"http://github.com/ttaylorr/dotfiles.git/info/lfs", false}, 23 | "http with root": {"http://github.com/ttaylorr/dotfiles", false}, 24 | "ssh with path prefix": {"ssh://github.com/ttaylorr/dotfiles.git/info/lfs", true}, 25 | "ssh with root": {"ssh://github.com/ttaylorr/dotfiles", true}, 26 | "ssh with user and path prefix": {"ssh://git@github.com/ttaylorr/dotfiles.git/info/lfs", true}, 27 | "ssh with user and root": {"ssh://git@github.com/ttaylorr/dotfiles", true}, 28 | } { 29 | t.Run(desc, c.Assert) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /errors/context.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | type withContext interface { 4 | Set(string, interface{}) 5 | Get(string) interface{} 6 | Del(string) 7 | Context() map[string]interface{} 8 | } 9 | 10 | // ErrorSetContext sets a value in the error's context. If the error has not 11 | // been wrapped, it does nothing. 12 | func SetContext(err error, key string, value interface{}) { 13 | if e, ok := err.(withContext); ok { 14 | e.Set(key, value) 15 | } 16 | } 17 | 18 | // ErrorGetContext gets a value from the error's context. If the error has not 19 | // been wrapped, it returns an empty string. 20 | func GetContext(err error, key string) interface{} { 21 | if e, ok := err.(withContext); ok { 22 | return e.Get(key) 23 | } 24 | return "" 25 | } 26 | 27 | // ErrorDelContext removes a value from the error's context. If the error has 28 | // not been wrapped, it does nothing. 29 | func DelContext(err error, key string) { 30 | if e, ok := err.(withContext); ok { 31 | e.Del(key) 32 | } 33 | } 34 | 35 | // ErrorContext returns the context map for an error if it is a wrappedError. 36 | // If it is not a wrappedError it will return an empty map. 37 | func Context(err error) map[string]interface{} { 38 | if e, ok := err.(withContext); ok { 39 | return e.Context() 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /tasklog/task.go: -------------------------------------------------------------------------------- 1 | package tasklog 2 | 3 | import "time" 4 | 5 | // Task is an interface which encapsulates an activity which can be logged. 6 | type Task interface { 7 | // Updates returns a channel which is written to with the current state 8 | // of the Task when an update is present. It is closed when the task is 9 | // complete. 10 | Updates() <-chan *Update 11 | 12 | // Throttled returns whether or not updates from this task should be 13 | // limited when being printed to a sink via *log.Logger. 14 | // 15 | // It is expected to return the same value for a given Task instance. 16 | Throttled() bool 17 | } 18 | 19 | // Update is a single message sent (S) from a Task at a given time (At). 20 | type Update struct { 21 | // S is the message sent in this update. 22 | S string 23 | // At is the time that this update was sent. 24 | At time.Time 25 | 26 | // Force determines if this update should not be throttled. 27 | Force bool 28 | } 29 | 30 | // Throttled determines whether this update should be throttled, based on the 31 | // given earliest time of the next update. The caller should determine how often 32 | // updates should be throttled. An Update with Force=true is never throttled. 33 | func (u *Update) Throttled(next time.Time) bool { 34 | return !(u.Force || u.At.After(next)) 35 | } 36 | -------------------------------------------------------------------------------- /tasklog/list_task.go: -------------------------------------------------------------------------------- 1 | package tasklog 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // ListTask is a Task implementation that logs all updates in a list where each 9 | // entry is line-delimited. 10 | // 11 | // For example: 12 | // 13 | // entry #1 14 | // entry #2 15 | // msg: ..., done. 16 | type ListTask struct { 17 | msg string 18 | ch chan *Update 19 | } 20 | 21 | // NewListTask instantiates a new *ListTask instance with the given message. 22 | func NewListTask(msg string) *ListTask { 23 | return &ListTask{ 24 | msg: msg, 25 | ch: make(chan *Update, 1), 26 | } 27 | } 28 | 29 | // Entry logs a line-delimited task entry. 30 | func (l *ListTask) Entry(update string) { 31 | l.ch <- &Update{ 32 | S: fmt.Sprintf("%s\n", update), 33 | At: time.Now(), 34 | } 35 | } 36 | 37 | func (l *ListTask) Complete() { 38 | l.ch <- &Update{ 39 | S: fmt.Sprintf("%s: ...", l.msg), 40 | At: time.Now(), 41 | } 42 | close(l.ch) 43 | } 44 | 45 | // Throttled implements the Task.Throttled function and ensures that all log 46 | // updates are printed to the sink. 47 | func (l *ListTask) Throttled() bool { return false } 48 | 49 | // Updates implements the Task.Updates function and returns a channel of updates 50 | // to log to the sink. 51 | func (l *ListTask) Updates() <-chan *Update { return l.ch } 52 | -------------------------------------------------------------------------------- /config/extension_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSortExtensions(t *testing.T) { 10 | m := map[string]Extension{ 11 | "baz": Extension{ 12 | "baz", 13 | "baz-clean %f", 14 | "baz-smudge %f", 15 | 2, 16 | }, 17 | "foo": Extension{ 18 | "foo", 19 | "foo-clean %f", 20 | "foo-smudge %f", 21 | 0, 22 | }, 23 | "bar": Extension{ 24 | "bar", 25 | "bar-clean %f", 26 | "bar-smudge %f", 27 | 1, 28 | }, 29 | } 30 | 31 | names := []string{"foo", "bar", "baz"} 32 | 33 | sorted, err := SortExtensions(m) 34 | 35 | assert.Nil(t, err) 36 | 37 | for i, ext := range sorted { 38 | name := names[i] 39 | assert.Equal(t, name, ext.Name) 40 | assert.Equal(t, name+"-clean %f", ext.Clean) 41 | assert.Equal(t, name+"-smudge %f", ext.Smudge) 42 | assert.Equal(t, i, ext.Priority) 43 | } 44 | } 45 | 46 | func TestSortExtensionsDuplicatePriority(t *testing.T) { 47 | m := map[string]Extension{ 48 | "foo": Extension{ 49 | "foo", 50 | "foo-clean %f", 51 | "foo-smudge %f", 52 | 0, 53 | }, 54 | "bar": Extension{ 55 | "bar", 56 | "bar-clean %f", 57 | "bar-smudge %f", 58 | 0, 59 | }, 60 | } 61 | 62 | sorted, err := SortExtensions(m) 63 | 64 | assert.NotNil(t, err) 65 | assert.Empty(t, sorted) 66 | } 67 | -------------------------------------------------------------------------------- /git/githistory/fixtures/octopus-merge.git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 8be6d64cddab01f53381e9feafe50d95ca5e6629 Taylor Blau 1496421999 -0600 commit (initial): initial commit 2 | 8be6d64cddab01f53381e9feafe50d95ca5e6629 8be6d64cddab01f53381e9feafe50d95ca5e6629 Taylor Blau 1496422012 -0600 checkout: moving from master to branch-a 3 | 8be6d64cddab01f53381e9feafe50d95ca5e6629 251e6b3461a3b5adc6bab694d5ae1abc878edf85 Taylor Blau 1496422020 -0600 commit: a.txt: initial 4 | 251e6b3461a3b5adc6bab694d5ae1abc878edf85 8be6d64cddab01f53381e9feafe50d95ca5e6629 Taylor Blau 1496422026 -0600 checkout: moving from branch-a to master 5 | 8be6d64cddab01f53381e9feafe50d95ca5e6629 8be6d64cddab01f53381e9feafe50d95ca5e6629 Taylor Blau 1496422029 -0600 checkout: moving from master to branch-b 6 | 8be6d64cddab01f53381e9feafe50d95ca5e6629 15805fe2044dc1a0508853e93d1a230bd94636be Taylor Blau 1496422035 -0600 commit: b.txt: initial 7 | 15805fe2044dc1a0508853e93d1a230bd94636be 8be6d64cddab01f53381e9feafe50d95ca5e6629 Taylor Blau 1496422041 -0600 checkout: moving from branch-b to master 8 | 8be6d64cddab01f53381e9feafe50d95ca5e6629 6c9ccaeb45446e3fa88cd5848a940fd34c18192b Taylor Blau 1496422044 -0600 merge branch-a branch-b: Merge made by the 'octopus' strategy. 9 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: git-lfs 3 | Source: https://github.com/git-lfs/git-lfs 4 | 5 | Files: * 6 | Copyright: 2013-2015 Github, Inc. 7 | License: Expat 8 | Copyright (c) GitHub, Inc. and Git LFS contributors 9 | . 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | . 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | . 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | -------------------------------------------------------------------------------- /tasklog/list_task_test.go: -------------------------------------------------------------------------------- 1 | package tasklog 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestListTaskCallsDoneWhenComplete(t *testing.T) { 10 | task := NewListTask("example") 11 | task.Complete() 12 | 13 | select { 14 | case update, ok := <-task.Updates(): 15 | assert.Equal(t, "example: ...", update.S) 16 | assert.True(t, ok, 17 | "tasklog: expected Updates() to remain open") 18 | default: 19 | t.Fatal("tasklog: expected update from *ListTask") 20 | } 21 | 22 | select { 23 | case update, ok := <-task.Updates(): 24 | assert.False(t, ok, 25 | "git/githistory.log: unexpected *ListTask.Update(): %s", update) 26 | default: 27 | t.Fatal("tasklog: expected *ListTask.Updates() to be closed") 28 | } 29 | } 30 | 31 | func TestListTaskWritesEntries(t *testing.T) { 32 | task := NewListTask("example") 33 | task.Entry("1") 34 | 35 | select { 36 | case update, ok := <-task.Updates(): 37 | assert.True(t, ok, 38 | "tasklog: expected ListTask.Updates() to remain open") 39 | assert.Equal(t, "1\n", update.S) 40 | default: 41 | t.Fatal("tasklog: expected task.Updates() to have an update") 42 | } 43 | } 44 | 45 | func TestListTaskIsNotThrottled(t *testing.T) { 46 | task := NewListTask("example") 47 | 48 | throttled := task.Throttled() 49 | 50 | assert.False(t, throttled, 51 | "tasklog: expected *ListTask to be Throttle()-d") 52 | } 53 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Andy Neff 2 | Artem V. Navrotskiy a.navrotskiy 3 | Artem V. Navrotskiy 4 | Brandon Keepers 5 | David Pursehouse 6 | Evan Priestley 7 | Josh Vera 8 | Lars Schneider 9 | Lee Reilly 10 | Noam Y. Tenne noamt 11 | Rick Olson rick 12 | Rick Olson risk danger olson 13 | Rick Olson Your Name 14 | Riku Lääkkölä 15 | Ryan Simmen 16 | Scott Barron rubyist 17 | Scott Barron Scott Barron 18 | Scott Richmond 19 | Sebastian Schuberth 20 | Taylor Blau 21 | William Hipschman Will 22 | -------------------------------------------------------------------------------- /tools/sync_writer.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import "io" 4 | 5 | // closeFn is the type of func Close() in the io.Closer interface. 6 | type closeFn func() error 7 | 8 | // syncFn is the type of func Sync() in the *os.File implementation. 9 | type syncFn func() error 10 | 11 | // SyncWriter provides a wrapper around an io.Writer that synchronizes all 12 | // write after they occur, if the underlying writer supports synchronization. 13 | type SyncWriter struct { 14 | w io.Writer 15 | 16 | closeFn closeFn 17 | syncFn syncFn 18 | } 19 | 20 | // NewSyncWriter returns a new instance of the *SyncWriter that sends all writes 21 | // to the given io.Writer. 22 | func NewSyncWriter(w io.Writer) *SyncWriter { 23 | sw := &SyncWriter{ 24 | w: w, 25 | } 26 | 27 | if sync, ok := w.(interface { 28 | Sync() error 29 | }); ok { 30 | sw.syncFn = sync.Sync 31 | } else { 32 | sw.syncFn = func() error { return nil } 33 | } 34 | 35 | if close, ok := w.(io.Closer); ok { 36 | sw.closeFn = close.Close 37 | } else { 38 | sw.closeFn = func() error { return nil } 39 | } 40 | 41 | return sw 42 | } 43 | 44 | // Write will write to the file and perform a Sync() if writing succeeds. 45 | func (w *SyncWriter) Write(b []byte) error { 46 | if _, err := w.w.Write(b); err != nil { 47 | return err 48 | } 49 | return w.syncFn() 50 | } 51 | 52 | // Close will call Close() on the underlying file 53 | func (w *SyncWriter) Close() error { 54 | return w.closeFn() 55 | } 56 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # Strip out CI environment variables which cause tests to fail. 5 | unset $(env | grep -E '^GIT(HUB)?_' | grep -v '^GIT_DEFAULT_HASH=' | sed -e 's/=.*$//') 6 | 7 | UNAME=$(uname -s) 8 | X="" 9 | if [[ $UNAME == MINGW* || $UNAME == MSYS* || $UNAME == CYGWIN* ]]; then 10 | X=".exe" 11 | WINDOWS=1 12 | fi 13 | 14 | # Build git-lfs-transfer from scutiger. 15 | cargo install --root t/scutiger scutiger-lfs 16 | 17 | # Set GOPATH if it isn't already set. 18 | eval "$(go env | grep GOPATH)" 19 | go install golang.org/x/tools/cmd/goimports@latest 20 | 21 | GOIMPORTS="$GOPATH/bin/goimports" 22 | 23 | make GOIMPORTS="$GOIMPORTS" && make GOIMPORTS="$GOIMPORTS" test 24 | 25 | # re-run test to ensure GIT_TRACE output doesn't leak into the git package 26 | GIT_TRACE=1 make GOIMPORTS="$GOIMPORTS" PKGS=git test 27 | 28 | pushd t >/dev/null 29 | PROVE="prove" 30 | PROVE_EXTRA_ARGS="-j9" 31 | VERBOSE_LOGS=1 make X="$X" clean 32 | VERBOSE_LOGS=1 make X="$X" PROVE="$PROVE" PROVE_EXTRA_ARGS="$PROVE_EXTRA_ARGS" 33 | popd >/dev/null 34 | 35 | echo "Looking for trailing whitespace..." 36 | if git grep -lE '[[:space:]]+$' | \ 37 | grep -vE '(^vendor/|\.git/(objects/|index)|\.(bat|ico|bmp)$)' 38 | then 39 | exit 1 40 | fi 41 | 42 | echo "Formatting files..." 43 | make GOIMPORTS="$GOIMPORTS" fmt 44 | 45 | echo "Looking for files that are not formatted correctly..." 46 | git status -s 47 | [ -z "$(git status --porcelain)" ] 48 | -------------------------------------------------------------------------------- /locking/cache_test.go: -------------------------------------------------------------------------------- 1 | package locking 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLockCache(t *testing.T) { 11 | var err error 12 | 13 | tmpf, err := os.CreateTemp("", "testCacheLock") 14 | assert.Nil(t, err) 15 | defer func() { 16 | os.Remove(tmpf.Name()) 17 | }() 18 | tmpf.Close() 19 | 20 | cache, err := NewLockCache(tmpf.Name()) 21 | assert.Nil(t, err) 22 | 23 | testLocks := []Lock{ 24 | Lock{Path: "folder/test1.dat", Id: "101"}, 25 | Lock{Path: "folder/test2.dat", Id: "102"}, 26 | Lock{Path: "root.dat", Id: "103"}, 27 | } 28 | 29 | for _, l := range testLocks { 30 | err = cache.Add(l) 31 | assert.Nil(t, err) 32 | } 33 | 34 | locks := cache.Locks() 35 | for _, l := range testLocks { 36 | assert.Contains(t, locks, l) 37 | } 38 | assert.Equal(t, len(testLocks), len(locks)) 39 | 40 | err = cache.RemoveByPath("folder/test2.dat") 41 | assert.Nil(t, err) 42 | 43 | locks = cache.Locks() 44 | // delete item 1 from test locls 45 | testLocks = append(testLocks[:1], testLocks[2:]...) 46 | for _, l := range testLocks { 47 | assert.Contains(t, locks, l) 48 | } 49 | assert.Equal(t, len(testLocks), len(locks)) 50 | 51 | err = cache.RemoveById("101") 52 | assert.Nil(t, err) 53 | 54 | locks = cache.Locks() 55 | testLocks = testLocks[1:] 56 | for _, l := range testLocks { 57 | assert.Contains(t, locks, l) 58 | } 59 | assert.Equal(t, len(testLocks), len(locks)) 60 | } 61 | -------------------------------------------------------------------------------- /t/cmd/lfstest-realpath.go: -------------------------------------------------------------------------------- 1 | //go:build testtools 2 | // +build testtools 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | func canonicalize(path string) (string, error) { 13 | left := path 14 | right := "" 15 | 16 | for { 17 | canon, err := filepath.EvalSymlinks(left) 18 | if err != nil && !os.IsNotExist(err) { 19 | return "", err 20 | } 21 | if err == nil { 22 | if right == "" { 23 | return canon, nil 24 | } 25 | return filepath.Join(canon, right), nil 26 | } 27 | // One component of our path is missing. Let's walk up a level 28 | // and canonicalize that and then append the remaining piece. 29 | full := filepath.Join(left, right) 30 | if right == "" { 31 | full = left 32 | } 33 | newleft := filepath.Clean(fmt.Sprintf("%s%c..", left, os.PathSeparator)) 34 | newright, err := filepath.Rel(newleft, full) 35 | if err != nil { 36 | return "", err 37 | } 38 | left = newleft 39 | right = newright 40 | } 41 | } 42 | 43 | func main() { 44 | if len(os.Args) != 2 { 45 | fmt.Fprintf(os.Stderr, "Usage: %s PATH\n", os.Args[0]) 46 | os.Exit(2) 47 | } 48 | 49 | path, err := filepath.Abs(os.Args[1]) 50 | if err != nil { 51 | fmt.Fprintf(os.Stderr, "Error creating absolute path: %v", err) 52 | os.Exit(3) 53 | } 54 | 55 | fullpath, err := canonicalize(path) 56 | if err != nil { 57 | fmt.Fprintf(os.Stderr, "Error canonicalizing: %v", err) 58 | os.Exit(4) 59 | } 60 | 61 | fmt.Println(fullpath) 62 | } 63 | -------------------------------------------------------------------------------- /tasklog/waiting_task_test.go: -------------------------------------------------------------------------------- 1 | package tasklog 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestWaitingTaskDisplaysWaitingStatus(t *testing.T) { 10 | task := NewWaitingTask("example") 11 | 12 | assert.Equal(t, "example: ...", (<-task.Updates()).S) 13 | } 14 | 15 | func TestWaitingTaskCallsDoneWhenComplete(t *testing.T) { 16 | task := NewWaitingTask("example") 17 | 18 | select { 19 | case v, ok := <-task.Updates(): 20 | if ok { 21 | assert.Equal(t, "example: ...", v.S) 22 | } else { 23 | t.Fatal("expected channel to be open") 24 | } 25 | default: 26 | } 27 | 28 | task.Complete() 29 | 30 | if _, ok := <-task.Updates(); ok { 31 | t.Fatalf("expected channel to be closed") 32 | } 33 | } 34 | 35 | func TestWaitingTaskPanicsWithMultipleDoneCalls(t *testing.T) { 36 | task := NewWaitingTask("example") 37 | 38 | task.Complete() 39 | 40 | defer func() { 41 | if err := recover(); err == nil { 42 | t.Fatal("tasklog: expected panic()") 43 | } else { 44 | if s, ok := err.(error); ok { 45 | assert.Equal(t, "close of closed channel", s.Error()) 46 | } else { 47 | t.Fatal("tasklog: expected panic() to implement error") 48 | } 49 | } 50 | }() 51 | 52 | task.Complete() 53 | } 54 | 55 | func TestWaitingTaskIsThrottled(t *testing.T) { 56 | task := NewWaitingTask("example") 57 | 58 | throttled := task.Throttled() 59 | 60 | assert.True(t, throttled, 61 | "tasklog: expected *WaitingTask to be Throttle()-d") 62 | } 63 | -------------------------------------------------------------------------------- /tr/tr.go: -------------------------------------------------------------------------------- 1 | package tr 2 | 3 | import ( 4 | "encoding/base64" 5 | "os" 6 | "strings" 7 | 8 | "github.com/leonelquinteros/gotext" 9 | ) 10 | 11 | //go:generate go run ../tr/trgen/trgen.go 12 | 13 | var Tr = gotext.NewLocale("/usr/share/locale", "en") 14 | 15 | var locales = make(map[string]string) 16 | 17 | func findLocale() string { 18 | vars := []string{"LC_ALL", "LC_MESSAGES", "LANG"} 19 | for _, varname := range vars { 20 | if val, ok := os.LookupEnv(varname); ok { 21 | return val 22 | } 23 | } 24 | return "" 25 | } 26 | 27 | func processLocale(locale string) []string { 28 | options := make([]string, 0, 2) 29 | // For example, split "en_DK.UTF-8" into "en_DK" and "UTF-8". 30 | pieces := strings.Split(locale, ".") 31 | options = append(options, pieces[0]) 32 | // For example, split "en_DK" into "en" and "DK". 33 | pieces = strings.Split(pieces[0], "_") 34 | if len(pieces) > 1 { 35 | options = append(options, pieces[0]) 36 | } 37 | return options 38 | } 39 | 40 | func InitializeLocale() { 41 | locale := findLocale() 42 | if len(locale) == 0 { 43 | return 44 | } 45 | Tr = gotext.NewLocale("/usr/share/locale", locale) 46 | Tr.AddDomain("git-lfs") 47 | for _, loc := range processLocale(locale) { 48 | if moData, ok := locales[loc]; ok { 49 | mo := gotext.NewMo() 50 | decodedData, err := base64.StdEncoding.DecodeString(moData) 51 | if err != nil { 52 | continue 53 | } 54 | mo.Parse(decodedData) 55 | Tr.AddTranslator("git-lfs", mo) 56 | return 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/man/git-lfs-fsck.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-fsck(1) 2 | 3 | == NAME 4 | 5 | git-lfs-fsck - Check GIT LFS files for consistency 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs fsck` [options] [revisions] 10 | 11 | == DESCRIPTION 12 | 13 | Checks all Git LFS files in the current HEAD for consistency. 14 | 15 | Corrupted files are moved to ".git/lfs/bad". 16 | 17 | The revisions may be specified as either a single committish, in which 18 | case only that commit is inspected; specified as a range of the form 19 | `A..B` (and only this form), in which case that range is inspected; or 20 | omitted entirely, in which case HEAD (and, for --objects, the index) is 21 | examined. 22 | 23 | The default is to perform all checks. 24 | 25 | In your Git configuration or in a `.lfsconfig` file, you may set 26 | `lfs.fetchexclude` to a comma-separated list of paths. If 27 | `lfs.fetchexclude` is defined, then any Git LFS files whose paths match 28 | one in that list will not be checked for consistency. Paths are matched 29 | using wildcard matching as per gitignore(5). 30 | 31 | == OPTIONS 32 | 33 | `--objects`:: 34 | Check that each object in HEAD matches its expected hash 35 | and that each object exists on disk. 36 | `--pointers`:: 37 | Check that each pointer is canonical and that each file 38 | which should be stored as a Git LFS file is so stored. 39 | `-d`:: 40 | `--dry-run`:: 41 | Perform checks, but do not move any corrupted files to `.git/lfs/bad`. 42 | 43 | == SEE ALSO 44 | 45 | git-lfs-ls-files(1), git-lfs-status(1), gitignore(5). 46 | 47 | Part of the git-lfs(1) suite. 48 | -------------------------------------------------------------------------------- /docs/proposals/ntlm.md: -------------------------------------------------------------------------------- 1 | # NTLM Authentication With Git-Lfs 2 | 3 | Enterprise users in a windows ecosystem are frequently required to use integrated auth. Basic auth does not meet their security requirements and setting up SSH on Windows is painful. 4 | 5 | There is an overview of NTLM at http://www.innovation.ch/personal/ronald/ntlm.html 6 | 7 | ### Implementation 8 | 9 | If the LFS server returns a "Www-Authenticate: NTLM" header, we will set lfs.{endpoint}.access to be ntlm and resubmit the http request. Subsequent requests will 10 | go through the ntlm auth flow. 11 | 12 | We will store NTLM credentials in the credential helper. When the user is prompted for their credentials they must use username:{DOMAIN}\{user} and password:{pass} 13 | 14 | The ntlm protocol will be handled by an ntlm.go class that hides the implementation of InitHandshake, Authenticate, and Challenge. This allows minimal changes to the existing 15 | client.go class. 16 | 17 | ### Tech 18 | 19 | There is a ntlm-go library available at https://github.com/ThomsonReutersEikon/go-ntlm that we can use. We will need to implement the Negotiate method and publish docs on what NTLM switches we support. I think simple user/pass/domain is best here so we avoid supporting a million settings with conflicting docs. 20 | 21 | ### Work 22 | 23 | Before supporting this as a mainstream scenario we should investigate making the CI work on windows so that we can successfully test changes. 24 | 25 | ### More Info 26 | 27 | You can see a hacked-together implementation of git lfs push with NTLM at https://github.com/WillHipschman/git-lfs/tree/ntlm 28 | -------------------------------------------------------------------------------- /t/t-credentials-no-prompt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | # these tests rely on GIT_TERMINAL_PROMPT to test properly 6 | ensure_git_version_isnt $VERSION_LOWER "2.3.0" 7 | 8 | begin_test "attempt private access without credential helper" 9 | ( 10 | set -e 11 | 12 | reponame="$(basename "$0" ".sh")" 13 | setup_remote_repo "$reponame" 14 | clone_repo "$reponame" without-creds 15 | 16 | git lfs track "*.dat" 17 | echo "hi" > hi.dat 18 | git add hi.dat 19 | git add .gitattributes 20 | git commit -m "initial commit" 21 | 22 | git config --global credential.helper lfsnoop 23 | git config credential.helper lfsnoop 24 | git config -l 25 | 26 | GIT_TERMINAL_PROMPT=0 git push origin main 2>&1 | tee push.log 27 | grep "Authorization error: $GITSERVER/$reponame" push.log || 28 | grep "Git credentials for $GITSERVER/$reponame not found" push.log 29 | ) 30 | end_test 31 | 32 | begin_test "askpass: push with bad askpass" 33 | ( 34 | set -e 35 | 36 | reponame="askpass-with-bad-askpass" 37 | setup_remote_repo "$reponame" 38 | clone_repo "$reponame" "$reponame" 39 | 40 | git lfs track "*.dat" 41 | echo "hello" > a.dat 42 | 43 | git add .gitattributes a.dat 44 | git commit -m "initial commit" 45 | 46 | git config "credential.helper" "" 47 | GIT_TERMINAL_PROMPT=0 GIT_ASKPASS="lfs-askpass-2" SSH_ASKPASS="dont-call-me" GIT_TRACE=1 git push origin main 2>&1 | tee push.log 48 | grep "failed to find GIT_ASKPASS command" push.log # attempt askpass 49 | grep "creds: git credential fill" push.log # attempt git credential 50 | ) 51 | end_test 52 | -------------------------------------------------------------------------------- /subprocess/cmd.go: -------------------------------------------------------------------------------- 1 | package subprocess 2 | 3 | import ( 4 | "io" 5 | "os/exec" 6 | ) 7 | 8 | // Thin wrapper around exec.Cmd. Takes care of pipe shutdown by 9 | // keeping an internal reference to any created pipes. Whenever 10 | // Cmd.Wait() is called, all created pipes are closed. 11 | type Cmd struct { 12 | *exec.Cmd 13 | 14 | pipes []io.Closer 15 | } 16 | 17 | func (c *Cmd) Run() error { 18 | c.trace() 19 | return c.Cmd.Run() 20 | } 21 | 22 | func (c *Cmd) Start() error { 23 | c.trace() 24 | return c.Cmd.Start() 25 | } 26 | 27 | func (c *Cmd) Output() ([]byte, error) { 28 | c.trace() 29 | return c.Cmd.Output() 30 | } 31 | 32 | func (c *Cmd) CombinedOutput() ([]byte, error) { 33 | c.trace() 34 | return c.Cmd.CombinedOutput() 35 | } 36 | 37 | func (c *Cmd) StdoutPipe() (io.ReadCloser, error) { 38 | stdout, err := c.Cmd.StdoutPipe() 39 | c.pipes = append(c.pipes, stdout) 40 | return stdout, err 41 | } 42 | 43 | func (c *Cmd) StderrPipe() (io.ReadCloser, error) { 44 | stderr, err := c.Cmd.StderrPipe() 45 | c.pipes = append(c.pipes, stderr) 46 | return stderr, err 47 | } 48 | 49 | func (c *Cmd) StdinPipe() (io.WriteCloser, error) { 50 | stdin, err := c.Cmd.StdinPipe() 51 | c.pipes = append(c.pipes, stdin) 52 | return stdin, err 53 | } 54 | 55 | func (c *Cmd) Wait() error { 56 | for _, pipe := range c.pipes { 57 | pipe.Close() 58 | } 59 | 60 | return c.Cmd.Wait() 61 | } 62 | 63 | func (c *Cmd) trace() { 64 | if len(c.Args) > 0 { 65 | Trace(c.Args[0], c.Args[1:]...) 66 | } else { 67 | Trace(c.Path) 68 | } 69 | } 70 | 71 | func newCmd(cmd *exec.Cmd) *Cmd { 72 | wrapped := &Cmd{Cmd: cmd} 73 | return wrapped 74 | } 75 | -------------------------------------------------------------------------------- /docs/man/git-lfs-pointer.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-pointer(1) 2 | 3 | == NAME 4 | 5 | git-lfs-pointer - Build, compare, and check pointers 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs pointer --file=path/to/file` + 10 | `git lfs pointer --file=path/to/file --pointer=path/to/pointer` + 11 | `git lfs pointer --file=path/to/file --stdin` + 12 | `git lfs pointer --check --file=path/to/file` 13 | 14 | == Description 15 | 16 | Builds and optionally compares generated pointer files to ensure 17 | consistency between different Git LFS implementations. 18 | 19 | == OPTIONS 20 | 21 | `-f`:: 22 | `--file`:: 23 | A local file to build the pointer from. 24 | `-p`:: 25 | `--pointer`:: 26 | A local file including the contents of a pointer generated from another 27 | implementation. This is compared to the pointer generated from `--file`. 28 | `--stdin`:: 29 | Reads the pointer from STDIN to compare with the pointer generated from 30 | `--file`. 31 | `--check`:: 32 | Reads the pointer from STDIN (if `--stdin` is given) or the filepath (if 33 | `--file`) is given. If neither or both of `--stdin` and `--file` are given, 34 | the invocation is invalid. Exits 0 if the data read is a valid Git LFS 35 | pointer. Exits 1 otherwise. 36 | `--strict`:: 37 | `--no-strict`:: 38 | In conjunction with `--check`, `--strict` verifies that the pointer is 39 | canonical; that is, it would be the one created by Git LFS. If it is not, 40 | exits 2. The default, for backwards compatibility, is `--no-strict`, but this 41 | may change in a future version. 42 | `--no-extensions`:: 43 | Don't print the extensions of the pointer. 44 | 45 | == SEE ALSO 46 | 47 | Part of the git-lfs(1) suite. 48 | -------------------------------------------------------------------------------- /tq/verify.go: -------------------------------------------------------------------------------- 1 | package tq 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/git-lfs/git-lfs/v3/lfsapi" 7 | "github.com/rubyist/tracerx" 8 | ) 9 | 10 | const ( 11 | maxVerifiesConfigKey = "lfs.transfer.maxverifies" 12 | defaultMaxVerifyAttempts = 3 13 | ) 14 | 15 | func verifyUpload(c *lfsapi.Client, remote string, t *Transfer) error { 16 | action, err := t.Actions.Get("verify") 17 | if err != nil { 18 | return err 19 | } 20 | if action == nil { 21 | return nil 22 | } 23 | 24 | req, err := http.NewRequest("POST", action.Href, nil) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | err = lfsapi.MarshalToRequest(req, struct { 30 | Oid string `json:"oid"` 31 | Size int64 `json:"size"` 32 | }{Oid: t.Oid, Size: t.Size}) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | req.Header.Set("Content-Type", "application/vnd.git-lfs+json") 38 | req.Header.Set("Accept", "application/vnd.git-lfs+json") 39 | for key, value := range action.Header { 40 | req.Header.Set(key, value) 41 | } 42 | 43 | mv := c.GitEnv().Int(maxVerifiesConfigKey, defaultMaxVerifyAttempts) 44 | mv = max(defaultMaxVerifyAttempts, mv) 45 | req = c.LogRequest(req, "lfs.verify") 46 | 47 | for i := 1; i <= mv; i++ { 48 | tracerx.Printf("tq: verify %s attempt #%d (max: %d)", t.Oid[:7], i, mv) 49 | 50 | var res *http.Response 51 | if t.Authenticated { 52 | res, err = c.Do(req) 53 | } else { 54 | res, err = c.DoWithAuth(remote, c.Endpoints.AccessFor(action.Href), req) 55 | } 56 | 57 | if err != nil { 58 | tracerx.Printf("tq: verify err: %+v", err.Error()) 59 | } else { 60 | err = res.Body.Close() 61 | break 62 | } 63 | } 64 | return err 65 | } 66 | -------------------------------------------------------------------------------- /lfs/config_test.go: -------------------------------------------------------------------------------- 1 | package lfs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/git-lfs/git-lfs/v3/config" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFetchPruneConfigDefault(t *testing.T) { 11 | cfg := config.NewFrom(config.Values{}) 12 | fp := NewFetchPruneConfig(cfg.Git) 13 | 14 | assert.Equal(t, 7, fp.FetchRecentRefsDays) 15 | assert.Equal(t, 0, fp.FetchRecentCommitsDays) 16 | assert.Equal(t, 3, fp.PruneOffsetDays) 17 | assert.True(t, fp.FetchRecentRefsIncludeRemotes) 18 | assert.Equal(t, 3, fp.PruneOffsetDays) 19 | assert.Equal(t, "origin", fp.PruneRemoteName) 20 | assert.False(t, fp.PruneVerifyRemoteAlways) 21 | assert.False(t, fp.PruneVerifyUnreachableAlways) 22 | } 23 | 24 | func TestFetchPruneConfigCustom(t *testing.T) { 25 | cfg := config.NewFrom(config.Values{ 26 | Git: map[string][]string{ 27 | "lfs.fetchrecentrefsdays": []string{"12"}, 28 | "lfs.fetchrecentremoterefs": []string{"false"}, 29 | "lfs.fetchrecentcommitsdays": []string{"9"}, 30 | "lfs.pruneoffsetdays": []string{"30"}, 31 | "lfs.pruneverifyremotealways": []string{"true"}, 32 | "lfs.pruneverifyunreachablealways": []string{"true"}, 33 | "lfs.pruneremotetocheck": []string{"upstream"}, 34 | }, 35 | }) 36 | fp := NewFetchPruneConfig(cfg.Git) 37 | 38 | assert.Equal(t, 12, fp.FetchRecentRefsDays) 39 | assert.Equal(t, 9, fp.FetchRecentCommitsDays) 40 | assert.False(t, fp.FetchRecentRefsIncludeRemotes) 41 | assert.Equal(t, 30, fp.PruneOffsetDays) 42 | assert.Equal(t, "upstream", fp.PruneRemoteName) 43 | assert.True(t, fp.PruneVerifyRemoteAlways) 44 | assert.True(t, fp.PruneVerifyUnreachableAlways) 45 | } 46 | -------------------------------------------------------------------------------- /lfs/gitscanner_remotes.go: -------------------------------------------------------------------------------- 1 | package lfs 2 | 3 | import ( 4 | "github.com/git-lfs/git-lfs/v3/git" 5 | "github.com/git-lfs/git-lfs/v3/tools" 6 | ) 7 | 8 | // calcSkippedRefs checks that locally cached versions of remote refs are still 9 | // present on the remote before they are used as a 'from' point. If the server 10 | // implements garbage collection and a remote branch had been deleted since we 11 | // last did 'git fetch --prune', then the objects in that branch may have also 12 | // been deleted on the server if unreferenced. If some refs are missing on the 13 | // remote, use a more explicit diff command. 14 | func calcSkippedRefs(remote string) []string { 15 | cachedRemoteRefs, _ := git.CachedRemoteRefs(remote) 16 | 17 | // Since CachedRemoteRefs() only returns branches, request that 18 | // RemoteRefs() ignore tags and also return only branches. 19 | actualRemoteRefs, _ := git.RemoteRefs(remote, false) 20 | 21 | // The list of remote refs can be very large, so convert them to 22 | // a set for faster lookups in the skip calculation loop. 23 | actualRemoteRefsSet := tools.NewStringSet() 24 | for _, ref := range actualRemoteRefs { 25 | actualRemoteRefsSet.Add(ref.Name) 26 | } 27 | 28 | // Only check for missing refs on remote; if the ref is different it has moved 29 | // forward probably, and if not and the ref has changed to a non-descendant 30 | // (force push) then that will cause a re-evaluation in a subsequent command. 31 | var skippedRefs []string 32 | for _, cachedRef := range cachedRemoteRefs { 33 | if actualRemoteRefsSet.Contains(cachedRef.Name) { 34 | skippedRefs = append(skippedRefs, "^"+cachedRef.Sha) 35 | } 36 | } 37 | return skippedRefs 38 | } 39 | -------------------------------------------------------------------------------- /tools/util_darwin_test.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package tools 5 | 6 | import ( 7 | "os" 8 | "path" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestCheckCloneFileSupported(t *testing.T) { 15 | as := assert.New(t) 16 | 17 | // Do 18 | ok, err := CheckCloneFileSupported(os.TempDir()) 19 | 20 | // Verify 21 | t.Logf("ok = %v, err = %v", ok, err) // Just logging for 1st element 22 | 23 | if !checkCloneFileSupported() { 24 | as.EqualError(err, "unsupported OS version. >= 10.12.x Sierra required") 25 | } 26 | } 27 | 28 | func TestCloneFile(t *testing.T) { 29 | as := assert.New(t) 30 | 31 | // Do 32 | ok, err := CloneFile(nil, nil) 33 | 34 | // Verify always no error and not ok 35 | as.NoError(err) 36 | as.False(ok) 37 | } 38 | 39 | func TestCloneFileByPath(t *testing.T) { 40 | if !cloneFileSupported { 41 | t.Skip("clone not supported on this platform") 42 | } 43 | 44 | src := path.Join(os.TempDir(), "src") 45 | t.Logf("src = %s", src) 46 | 47 | dst := path.Join(os.TempDir(), "dst") 48 | t.Logf("dst = %s", dst) 49 | 50 | as := assert.New(t) 51 | 52 | // Precondition 53 | err := os.WriteFile(src, []byte("TEST"), 0666) 54 | as.NoError(err) 55 | 56 | // Do 57 | ok, err := CloneFileByPath(dst, src) 58 | if err != nil { 59 | if cloneFileError, ok := err.(*CloneFileError); ok && cloneFileError.Unsupported { 60 | t.Log(err) 61 | t.Skip("tmp file is not support clonefile in this os installation.") 62 | } 63 | t.Error(err) 64 | } 65 | 66 | // Verify 67 | as.NoError(err) 68 | as.True(ok) 69 | 70 | dstContents, err := os.ReadFile(dst) 71 | as.NoError(err) 72 | as.Equal("TEST", string(dstContents)) 73 | } 74 | -------------------------------------------------------------------------------- /t/t-progress.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | reponame="$(basename "$0" ".sh")" 6 | 7 | begin_test "GIT_LFS_PROGRESS" 8 | ( 9 | set -e 10 | setup_remote_repo "$reponame" 11 | clone_repo "$reponame" repo 12 | 13 | git lfs track "*.dat" 14 | echo "a" > a.dat 15 | echo "b" > b.dat 16 | echo "c" > c.dat 17 | echo "d" > d.dat 18 | echo "e" > e.dat 19 | git add .gitattributes *.dat 20 | git commit -m "add files" 21 | git push origin main 2>&1 | tee push.log 22 | grep "Uploading LFS objects: 100% (5/5), 10 B" push.log 23 | 24 | cd .. 25 | GIT_LFS_PROGRESS="$TRASHDIR/progress.log" git lfs clone "$GITSERVER/$reponame" clone 26 | cat progress.log 27 | grep "download 1/5" progress.log 28 | grep "download 2/5" progress.log 29 | grep "download 3/5" progress.log 30 | grep "download 4/5" progress.log 31 | grep "download 5/5" progress.log 32 | 33 | GIT_LFS_SKIP_SMUDGE=1 git clone "$GITSERVER/$reponame" clone2 34 | cd clone2 35 | 36 | rm -rf "$TRASHDIR/progress.log" .git/lfs/objects 37 | GIT_LFS_PROGRESS="$TRASHDIR/progress.log" git lfs fetch --all 38 | cat ../progress.log 39 | grep "download 1/5" ../progress.log 40 | grep "download 2/5" ../progress.log 41 | grep "download 3/5" ../progress.log 42 | grep "download 4/5" ../progress.log 43 | grep "download 5/5" ../progress.log 44 | 45 | rm -rf "$TRASHDIR/progress.log" 46 | GIT_LFS_PROGRESS="$TRASHDIR/progress.log" git lfs checkout 47 | cat ../progress.log 48 | grep "checkout 1/5" ../progress.log 49 | grep "checkout 2/5" ../progress.log 50 | grep "checkout 3/5" ../progress.log 51 | grep "checkout 4/5" ../progress.log 52 | grep "checkout 5/5" ../progress.log 53 | ) 54 | end_test 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **System environment** 24 | The version of your operating system, plus any relevant information about platform or configuration (e.g., container or CI usage, Cygwin, WSL, or non-Basic authentication). If relevant, include the output of `git config -l` as a code block. 25 | 26 | Please also mention the usage of any proxy, including any TLS MITM device or non-default antivirus or firewall. 27 | 28 | **Output of `git lfs env`** 29 | The output of running `git lfs env` as a code block. 30 | 31 | **Additional context** 32 | Any other relevant context about the problem here. 33 | 34 | If you're having problems trying to push or pull data, please run the command with `GIT_TRACE=1 GIT_TRANSFER_TRACE=1 GIT_CURL_VERBOSE=1` and include it inline or attach it as a text file. In a bash or other POSIX shell, you can simply prepend this string and a space to the command. 35 | 36 | 43 | -------------------------------------------------------------------------------- /git/ls_tree_scanner_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type genericScanner interface { 11 | Err() error 12 | Scan() bool 13 | } 14 | 15 | func assertNextScan(t *testing.T, scanner genericScanner) { 16 | assert.True(t, scanner.Scan()) 17 | assert.Nil(t, scanner.Err()) 18 | } 19 | 20 | func assertScannerDone(t *testing.T, scanner genericScanner) { 21 | assert.False(t, scanner.Scan()) 22 | assert.Nil(t, scanner.Err()) 23 | } 24 | 25 | func TestLsTreeParser(t *testing.T) { 26 | stdout := "100644 blob d899f6551a51cf19763c5955c7a06a2726f018e9 42 .gitattributes\000100644 blob 4d343e022e11a8618db494dc3c501e80c7e18197 126 PB SCN 16 Odhrán.wav" 27 | scanner := NewLsTreeScanner(strings.NewReader(stdout)) 28 | 29 | assertNextTreeBlob(t, scanner, "d899f6551a51cf19763c5955c7a06a2726f018e9", ".gitattributes") 30 | assertNextTreeBlob(t, scanner, "4d343e022e11a8618db494dc3c501e80c7e18197", "PB SCN 16 Odhrán.wav") 31 | assertScannerDone(t, scanner) 32 | } 33 | 34 | func assertNextTreeBlob(t *testing.T, scanner *LsTreeScanner, oid, filename string) { 35 | assertNextScan(t, scanner) 36 | b := scanner.TreeBlob() 37 | assert.NotNil(t, b) 38 | assert.Equal(t, oid, b.Oid) 39 | assert.Equal(t, filename, b.Filename) 40 | } 41 | 42 | func BenchmarkLsTreeParser(b *testing.B) { 43 | stdout := "100644 blob d899f6551a51cf19763c5955c7a06a2726f018e9 42 .gitattributes\000100644 blob 4d343e022e11a8618db494dc3c501e80c7e18197 126 PB SCN 16 Odhrán.wav" 44 | 45 | // run the Fib function b.N times 46 | for n := 0; n < b.N; n++ { 47 | scanner := NewLsTreeScanner(strings.NewReader(stdout)) 48 | for scanner.Scan() { 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /t/t-duplicate-oids.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "multiple revs with same OID get pushed once" 6 | ( 7 | set -e 8 | 9 | reponame="multiple-revs-one-oid" 10 | setup_remote_repo "$reponame" 11 | clone_repo "$reponame" "$reponame" 12 | 13 | git lfs track "*.dat" 14 | git add .gitattributes 15 | git commit -m "initial commit" 16 | 17 | contents="contents" 18 | contents_oid="$(calc_oid "$contents")" 19 | 20 | # Stash the contents of the file that we want to commit in .git/lfs/objects. 21 | object_dir="$(echo $contents_oid \ 22 | | awk '{ print substr($0, 1, 2) "/" substr($0, 3, 2) }')" 23 | mkdir -p ".git/lfs/objects/$object_dir" 24 | printf "%s" "$contents" > ".git/lfs/objects/$object_dir/$contents_oid" 25 | 26 | # Create a pointer with the old "http://git-media.io" spec 27 | legacy_pointer="$(pointer $contents_oid 8 http://git-media.io/v/2)" 28 | # Create a pointer with the latest spec to create a modification, but leave 29 | # the OID untouched. 30 | latest_pointer="$(pointer $contents_oid 8)" 31 | 32 | # Commit the legacy pointer 33 | printf "%s" "$legacy_pointer" > a.dat 34 | git add a.dat 35 | git commit -m "commit legacy" 36 | 37 | # Commit the new pointer, causing a diff on a.dat, but leaving the OID 38 | # unchanged. 39 | printf "%s" "$latest_pointer" > a.dat 40 | git add a.dat 41 | git commit -m "commit latest" 42 | 43 | # Delay the push until here, so the server doesn't have a copy of the OID that 44 | # we're trying to push. 45 | git push origin main 2>&1 | tee push.log 46 | grep "Uploading LFS objects: 100% (1/1), 8 B" push.log 47 | 48 | assert_server_object "$reponame" "$contents_oid" 49 | ) 50 | end_test 51 | -------------------------------------------------------------------------------- /tools/util_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package tools 5 | 6 | import ( 7 | "io" 8 | "os" 9 | 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | // CheckCloneFileSupported runs explicit test of clone file on supplied directory. 14 | // This function creates some (src and dst) file in the directory and remove after test finished. 15 | // 16 | // If check failed (e.g. directory is read-only), returns err. 17 | func CheckCloneFileSupported(dir string) (supported bool, err error) { 18 | src, err := os.CreateTemp(dir, "src") 19 | if err != nil { 20 | return false, err 21 | } 22 | defer func() { 23 | src.Close() 24 | os.Remove(src.Name()) 25 | }() 26 | 27 | dst, err := os.CreateTemp(dir, "dst") 28 | if err != nil { 29 | return false, err 30 | } 31 | defer func() { 32 | dst.Close() 33 | os.Remove(dst.Name()) 34 | }() 35 | 36 | if ok, err := CloneFile(dst, src); err != nil { 37 | return false, err 38 | } else { 39 | return ok, nil 40 | } 41 | } 42 | 43 | func CloneFile(writer io.Writer, reader io.Reader) (bool, error) { 44 | fdst, fdstFound := writer.(*os.File) 45 | fsrc, fsrcFound := reader.(*os.File) 46 | if fdstFound && fsrcFound { 47 | if err := unix.IoctlFileClone(int(fdst.Fd()), int(fsrc.Fd())); err != nil { 48 | return false, err 49 | } 50 | return true, nil 51 | } 52 | return false, nil 53 | } 54 | 55 | func CloneFileByPath(dst, src string) (bool, error) { 56 | srcFile, err := os.Open(src) 57 | if err != nil { 58 | return false, err 59 | } 60 | defer srcFile.Close() 61 | dstFile, err := os.Create(dst) //truncating, it if it already exists. 62 | if err != nil { 63 | return false, err 64 | } 65 | defer dstFile.Close() 66 | 67 | return CloneFile(dstFile, srcFile) 68 | } 69 | -------------------------------------------------------------------------------- /t/t-ext.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "ext" 6 | ( 7 | set -e 8 | 9 | # no need to setup a remote repo, since this test does not need to push or pull 10 | 11 | mkdir ext 12 | cd ext 13 | git init 14 | 15 | git config lfs.extension.foo.clean "foo-clean %f" 16 | git config lfs.extension.foo.smudge "foo-smudge %f" 17 | git config lfs.extension.foo.priority 0 18 | 19 | git config lfs.extension.bar.clean "bar-clean %f" 20 | git config lfs.extension.bar.smudge "bar-smudge %f" 21 | git config lfs.extension.bar.priority 1 22 | 23 | git config lfs.extension.baz.clean "baz-clean %f" 24 | git config lfs.extension.baz.smudge "baz-smudge %f" 25 | git config lfs.extension.baz.priority 2 26 | 27 | fooExpected="Extension: foo 28 | clean = foo-clean %f 29 | smudge = foo-smudge %f 30 | priority = 0" 31 | 32 | barExpected="Extension: bar 33 | clean = bar-clean %f 34 | smudge = bar-smudge %f 35 | priority = 1" 36 | 37 | bazExpected="Extension: baz 38 | clean = baz-clean %f 39 | smudge = baz-smudge %f 40 | priority = 2" 41 | 42 | actual=$(git lfs ext list foo) 43 | [ "$actual" = "$fooExpected" ] 44 | 45 | actual=$(git lfs ext list bar) 46 | [ "$actual" = "$barExpected" ] 47 | 48 | actual=$(git lfs ext list baz) 49 | [ "$actual" = "$bazExpected" ] 50 | 51 | actual=$(git lfs ext list foo bar) 52 | expected=$(printf "%s\n%s" "$fooExpected" "$barExpected") 53 | [ "$actual" = "$expected" ] 54 | 55 | actual=$(git lfs ext list) 56 | expected=$(printf "%s\n%s\n%s" "$fooExpected" "$barExpected" "$bazExpected") 57 | [ "$actual" = "$expected" ] 58 | 59 | actual=$(git lfs ext) 60 | [ "$actual" = "$expected" ] 61 | ) 62 | end_test 63 | -------------------------------------------------------------------------------- /tasklog/simple_task_test.go: -------------------------------------------------------------------------------- 1 | package tasklog 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestSimpleTaskLogLogsUpdates(t *testing.T) { 11 | task := NewSimpleTask() 12 | 13 | var updates []*Update 14 | 15 | go func() { 16 | for update := range task.Updates() { 17 | updates = append(updates, update) 18 | } 19 | task.OnComplete() 20 | }() 21 | 22 | task.Log("Hello, world") 23 | task.Complete() 24 | 25 | require.Len(t, updates, 1) 26 | assert.Equal(t, "Hello, world", updates[0].S) 27 | } 28 | 29 | func TestSimpleTaskLogfLogsFormattedUpdates(t *testing.T) { 30 | task := NewSimpleTask() 31 | 32 | var updates []*Update 33 | 34 | go func() { 35 | for update := range task.Updates() { 36 | updates = append(updates, update) 37 | } 38 | task.OnComplete() 39 | }() 40 | 41 | task.Logf("Hello, world (%d)", 3+4) 42 | task.Complete() 43 | 44 | require.Len(t, updates, 1) 45 | assert.Equal(t, "Hello, world (7)", updates[0].S) 46 | } 47 | 48 | func TestSimpleTaskCompleteClosesUpdates(t *testing.T) { 49 | task := NewSimpleTask() 50 | 51 | select { 52 | case <-task.Updates(): 53 | t.Fatalf("tasklog: unexpected update from *SimpleTask") 54 | default: 55 | } 56 | 57 | go func() { 58 | <-task.Updates() 59 | task.OnComplete() 60 | }() 61 | 62 | task.Complete() 63 | 64 | if _, ok := <-task.Updates(); ok { 65 | t.Fatalf("tasklog: expected (*SimpleTask).Updates() to be closed") 66 | } 67 | } 68 | 69 | func TestSimpleTaskIsNotThrottled(t *testing.T) { 70 | task := NewSimpleTask() 71 | 72 | throttled := task.Throttled() 73 | 74 | assert.False(t, throttled, 75 | "tasklog: expected *SimpleTask not to be Throttle()-d") 76 | } 77 | -------------------------------------------------------------------------------- /commands/path_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package commands 5 | 6 | import ( 7 | "path/filepath" 8 | "regexp" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/git-lfs/git-lfs/v3/subprocess" 13 | ) 14 | 15 | var ( 16 | winBashPrefix string 17 | winBashMu sync.Mutex 18 | winBashRe *regexp.Regexp 19 | ) 20 | 21 | func osLineEnding() string { 22 | return "\r\n" 23 | } 24 | 25 | // cleanRootPath replaces the windows root path prefix with a unix path prefix: 26 | // "/". Git Bash (provided with Git For Windows) expands a path like "/foo" to 27 | // the actual Windows directory, but with forward slashes. You can see this 28 | // for yourself: 29 | // 30 | // $ git /foo 31 | // git: 'C:/Program Files/Git/foo' is not a git command. See 'git --help'. 32 | // 33 | // You can check the path with `pwd -W`: 34 | // 35 | // $ cd / 36 | // $ pwd 37 | // / 38 | // $ pwd -W 39 | // c:/Program Files/Git 40 | func cleanRootPath(pattern string) string { 41 | winBashMu.Lock() 42 | defer winBashMu.Unlock() 43 | 44 | // check if path starts with windows drive letter 45 | if !winPathHasDrive(pattern) { 46 | return pattern 47 | } 48 | 49 | if len(winBashPrefix) < 1 { 50 | // cmd.Path is something like C:\Program Files\Git\usr\bin\pwd.exe 51 | cmd, err := subprocess.ExecCommand("pwd") 52 | if err != nil { 53 | return pattern 54 | } 55 | winBashPrefix = strings.Replace(filepath.Dir(filepath.Dir(filepath.Dir(cmd.Path))), `\`, "/", -1) + "/" 56 | } 57 | 58 | return strings.Replace(pattern, winBashPrefix, "/", 1) 59 | } 60 | 61 | func winPathHasDrive(pattern string) bool { 62 | if winBashRe == nil { 63 | winBashRe = regexp.MustCompile(`\A\w{1}:[/\/]`) 64 | } 65 | 66 | return winBashRe.MatchString(pattern) 67 | } 68 | -------------------------------------------------------------------------------- /docs/man/git-lfs-push.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-push(1) 2 | 3 | == NAME 4 | 5 | git-lfs-push - Push queued large files to the Git LFS endpoint 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs push` [options] [...] + 10 | `git lfs push` [...] + 11 | `git lfs push` [options] --stdin 12 | `git lfs push` --object-id [...] 13 | `git lfs push` --object-id --stdin 14 | 15 | == DESCRIPTION 16 | 17 | Upload Git LFS files to the configured endpoint for the current Git 18 | remote. By default, it filters out objects that are already referenced 19 | by the local clone of the remote. 20 | 21 | == OPTIONS 22 | 23 | `-d`:: 24 | `--dry-run`:: 25 | Print the files that would be pushed, without actually pushing them. 26 | `-a`:: 27 | `--all`:: 28 | This pushes all objects to the remote that are referenced by any commit 29 | reachable from the refs provided as arguments. If no refs are provided, then 30 | all local refs are pushed. Note that this behavior differs from that of 31 | git-lfs-fetch(1) when its `--all` option is used; in that case, all refs are 32 | fetched, including refs other than those under `refs/heads` and `refs/tags`. 33 | If you are migrating a repository with these commands, make sure to run `git 34 | lfs push` for any additional remote refs that contain Git LFS objects not 35 | reachable from your local refs. 36 | `-o`:: 37 | `--object-id`:: 38 | This pushes only the object OIDs listed at the end of the command, separated 39 | by spaces. 40 | `--stdin`:: 41 | Read a list of newline-delimited refs (or object IDs when using `--object-id`) 42 | from standard input instead of the command line. 43 | 44 | == SEE ALSO 45 | 46 | git-lfs-fetch(1), git-lfs-pre-push(1). 47 | 48 | Part of the git-lfs(1) suite. 49 | -------------------------------------------------------------------------------- /commands/command_post_merge.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/git-lfs/git-lfs/v3/tr" 7 | "github.com/rubyist/tracerx" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // postMergeCommand is run through Git's post-merge hook. 12 | // 13 | // This hook checks that files which are lockable and not locked are made read-only, 14 | // optimising that as best it can based on the available information. 15 | func postMergeCommand(cmd *cobra.Command, args []string) { 16 | if len(args) != 1 { 17 | Print(tr.Tr.Get("This should be run through Git's post-merge hook. Run `git lfs update` to install it.")) 18 | os.Exit(1) 19 | } 20 | 21 | // Skip entire hook if lockable read only feature is disabled 22 | if !cfg.SetLockableFilesReadOnly() { 23 | os.Exit(0) 24 | } 25 | 26 | requireGitVersion() 27 | 28 | lockClient := newLockClient() 29 | 30 | // Skip this hook if no lockable patterns have been configured 31 | if len(lockClient.GetLockablePatterns()) == 0 { 32 | os.Exit(0) 33 | } 34 | 35 | // The only argument this hook receives is a flag indicating whether the 36 | // merge was a squash merge; we don't know what files changed. 37 | // Whether it's squash or not is irrelevant, either way it could have 38 | // reset the read-only flag on files that got merged. 39 | 40 | tracerx.Printf("post-merge: checking write flags for all lockable files") 41 | // Sadly we don't get any information about what files were checked out, 42 | // so we have to check the entire repo 43 | err := lockClient.FixAllLockableFileWriteFlags() 44 | if err != nil { 45 | LoggedError(err, tr.Tr.Get("Warning: post-merge locked file check failed: %v", err)) 46 | } 47 | } 48 | 49 | func init() { 50 | RegisterCommand("post-merge", postMergeCommand, nil) 51 | } 52 | -------------------------------------------------------------------------------- /t/t-commit-delete-push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "commit, delete, then push" 6 | ( 7 | set -e 8 | 9 | reponame="$(basename "$0" ".sh")" 10 | setup_remote_repo "$reponame" 11 | clone_repo "$reponame" repo 12 | 13 | git lfs track "*.dat" 14 | 15 | deleted_oid=$(calc_oid "deleted\n") 16 | echo "deleted" > deleted.dat 17 | git add deleted.dat .gitattributes 18 | git commit -m "add deleted file" 19 | 20 | git lfs push origin main --dry-run | grep "push ee31ef227442936872744b50d3297385c08b40ffc7baeaf34a39e6d81d6cd9ee => deleted.dat" 21 | 22 | assert_pointer "main" "deleted.dat" "$deleted_oid" 8 23 | 24 | added_oid=$(calc_oid "added\n") 25 | echo "added" > added.dat 26 | git add added.dat 27 | git commit -m "add file" 28 | 29 | git lfs push origin main --dry-run | tee dryrun.log 30 | grep "push ee31ef227442936872744b50d3297385c08b40ffc7baeaf34a39e6d81d6cd9ee => deleted.dat" dryrun.log 31 | grep "push 3428719b7688c78a0cc8ba4b9e80b4e464c815fbccfd4b20695a15ffcefc22af => added.dat" dryrun.log 32 | 33 | git rm deleted.dat 34 | git commit -m "did not need deleted.dat after all" 35 | 36 | git lfs push origin main --dry-run 2>&1 | tee dryrun.log 37 | grep "push ee31ef227442936872744b50d3297385c08b40ffc7baeaf34a39e6d81d6cd9ee => deleted.dat" dryrun.log 38 | grep "push 3428719b7688c78a0cc8ba4b9e80b4e464c815fbccfd4b20695a15ffcefc22af => added.dat" dryrun.log 39 | 40 | git log 41 | git push origin main 2>&1 > push.log || { 42 | cat push.log 43 | git lfs logs last 44 | exit 1 45 | } 46 | grep "(2 of 2 files)" push.log | cat push.log 47 | 48 | assert_server_object "$reponame" "$deleted_oid" 49 | assert_server_object "$reponame" "$added_oid" 50 | ) 51 | end_test 52 | -------------------------------------------------------------------------------- /t/t-submodule-recurse.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | reponame="submodule-recurse-test-repo" 5 | submodname="submodule-recurse-test-submodule" 6 | 7 | begin_test "submodule with submodule.recurse = true" 8 | ( 9 | set -e 10 | 11 | setup_remote_repo "$reponame" 12 | setup_remote_repo "$submodname" 13 | 14 | clone_repo "$submodname" submodule 15 | 16 | git lfs track "*.dat" 2>&1 | tee track.log 17 | grep "Tracking \"\*.dat\"" track.log 18 | 19 | echo "foo" > file.dat 20 | git add .gitattributes file.dat 21 | git commit -a -m "add file" 22 | git push origin main 23 | subcommit1=$(git rev-parse HEAD) 24 | 25 | echo "bar" > file.dat 26 | git add file.dat 27 | git commit -a -m "update file" 28 | git push origin main 29 | subcommit2=$(git rev-parse HEAD) 30 | 31 | clone_repo "$reponame" repo 32 | git submodule add "$GITSERVER/$submodname" submodule 33 | git submodule update --init --recursive 34 | git -C submodule reset --hard "$subcommit1" 35 | git add .gitmodules submodule 36 | git commit -m "add submodule" 37 | git push origin main 38 | 39 | git checkout -b feature 40 | git -C submodule reset --hard "$subcommit2" 41 | git add .gitmodules submodule 42 | git commit -m "update submodule" 43 | git push origin feature 44 | 45 | clone_repo "$reponame" repo-no-recurse 46 | git submodule update --init --recursive 47 | git checkout feature 48 | 49 | if [[ -d "submodule/lfs/logs" ]] 50 | then 51 | exit 1 52 | fi 53 | 54 | clone_repo "$reponame" repo-recurse 55 | git config submodule.recurse true 56 | git submodule update --init --recursive 57 | git checkout feature 58 | 59 | if [[ -d "submodule/lfs/logs" ]] 60 | then 61 | exit 1 62 | fi 63 | ) 64 | end_test 65 | -------------------------------------------------------------------------------- /t/t-expired.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | declare -a expiration_types=("absolute" "relative" "both") 6 | 7 | for typ in "${expiration_types[@]}"; do 8 | begin_test "expired action ($typ time)" 9 | ( 10 | set -e 11 | 12 | reponame="expired-$typ" 13 | setup_remote_repo "$reponame" 14 | clone_repo "$reponame" "$reponame" 15 | 16 | contents="contents" 17 | contents_oid="$(calc_oid "$contents")" 18 | 19 | git lfs track "*.dat" 20 | git add .gitattributes 21 | git commit -m "initial commit" 22 | 23 | printf "%s" "$contents" > a.dat 24 | 25 | git add a.dat 26 | git commit -m "add a.dat" 27 | 28 | GIT_TRACE=1 git push origin main 2>&1 | tee push.log 29 | if [ "0" -eq "${PIPESTATUS[0]}" ]; then 30 | echo >&2 "fatal: expected push to fail, didn't" 31 | exit 1 32 | fi 33 | 34 | refute_server_object "$reponame" "$contents_oid" 35 | ) 36 | end_test 37 | done 38 | 39 | for typ in "${expiration_types[@]}"; do 40 | begin_test "ssh expired ($typ time; git-lfs-authenticate)" 41 | ( 42 | set -e 43 | 44 | reponame="ssh-expired-$typ" 45 | setup_remote_repo "$reponame" 46 | clone_repo "$reponame" "$reponame" 47 | 48 | sshurl="${GITSERVER/http:\/\//ssh://git@}/$reponame" 49 | git config lfs.url "$sshurl" 50 | 51 | contents="contents" 52 | contents_oid="$(calc_oid "$contents")" 53 | 54 | git lfs track "*.dat" 55 | git add .gitattributes 56 | git commit -m "initial commit" 57 | 58 | printf "%s" "$contents" > a.dat 59 | 60 | git add a.dat 61 | git commit -m "add a.dat" 62 | 63 | GIT_TRACE=1 git push origin main 2>&1 | tee push.log 64 | grep "ssh cache expired" push.log 65 | ) 66 | end_test 67 | done 68 | -------------------------------------------------------------------------------- /docs/man/git-lfs-uninstall.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-uninstall(1) 2 | 3 | == NAME 4 | 5 | git-lfs-uninstall - Remove Git LFS configuration 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs uninstall` 10 | 11 | == DESCRIPTION 12 | 13 | Perform the following actions to remove the Git LFS configuration: 14 | 15 | * Remove the "lfs" clean and smudge filters from the global Git config. 16 | * Uninstall the Git LFS pre-push hook if run from inside a Git 17 | repository. 18 | 19 | == OPTIONS 20 | 21 | `-l`:: 22 | `--local`:: 23 | Removes the "lfs" smudge and clean filters from the local repository's git 24 | config, instead of the global git config (~/.gitconfig). 25 | `-w`:: 26 | `--worktree`:: 27 | Removes the "lfs" smudge and clean filters from the current working tree's git 28 | config, instead of the global git config (~/.gitconfig) or local repository's 29 | git config ($GIT_DIR/config). If multiple working trees are in use, the Git 30 | config extension `worktreeConfig` must be enabled to use this option. If only 31 | one working tree is in use, `--worktree` has the same effect as `--local`. 32 | This option is only available if the installed Git version is at least 2.20.0 33 | and therefore supports the "worktreeConfig" extension. 34 | `--system`:: 35 | Removes the "lfs" smudge and clean filters from the system git config, instead 36 | of the global git config (~/.gitconfig). 37 | `--file=`:: 38 | Removes the `lfs` smudge and clean filters from the Git configuration file 39 | specified by the `` argument. 40 | `--skip-repo`:: 41 | Skips cleanup of the local repo; use if you want to uninstall the global lfs 42 | filters but not make changes to the current repo. 43 | 44 | == SEE ALSO 45 | 46 | git-lfs-install(1), git-worktree(1). 47 | 48 | Part of the git-lfs(1) suite. 49 | -------------------------------------------------------------------------------- /t/t-batch-error-handling.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "batch error handling" 6 | ( 7 | set -e 8 | 9 | # This initializes a new bare git repository in test/remote. 10 | # These remote repositories are global to every test, so keep the names 11 | # unique. 12 | reponame="badbatch" # Server looks for the "badbatch" repo, returns a 203 status 13 | setup_remote_repo "$reponame" 14 | 15 | # Clone the repository from the test Git server. This is empty, and will be 16 | # used to test a "git pull" below. The repo is cloned to $TRASHDIR/clone 17 | clone_repo "$reponame" clone 18 | 19 | # Clone the repository again to $TRASHDIR/repo. This will be used to commit 20 | # and push objects. 21 | clone_repo "$reponame" repo 22 | 23 | # This executes Git LFS from the local repo that was just cloned. 24 | git lfs track "*.dat" 2>&1 | tee track.log 25 | grep "Tracking \"\*.dat\"" track.log 26 | 27 | contents="a" 28 | contents_oid=$(calc_oid "$contents") 29 | 30 | printf "%s" "$contents" > a.dat 31 | git add a.dat 32 | git add .gitattributes 33 | git commit -m "add a.dat" 2>&1 | tee commit.log 34 | grep "main (root-commit)" commit.log 35 | grep "2 files changed" commit.log 36 | grep "create mode 100644 a.dat" commit.log 37 | grep "create mode 100644 .gitattributes" commit.log 38 | 39 | [ "a" = "$(cat a.dat)" ] 40 | 41 | # This is a small shell function that runs several git commands together. 42 | assert_pointer "main" "a.dat" "$contents_oid" 1 43 | 44 | refute_server_object "$reponame" "$contents_oid" 45 | 46 | # This pushes to the remote repository set up at the top of the test. 47 | git push origin main 2>&1 | tee push.log 48 | grep "Unable to parse HTTP response" push.log 49 | ) 50 | end_test 51 | -------------------------------------------------------------------------------- /t/t-zero-len-file.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | reponame="$(basename "$0" ".sh")" 6 | 7 | begin_test "push zero len file" 8 | ( 9 | set -e 10 | 11 | setup_remote_repo "$reponame" 12 | 13 | clone_repo "$reponame" repo 14 | 15 | git lfs track "*.dat" 16 | touch empty.dat 17 | 18 | contents="full" 19 | contents_oid=$(calc_oid "$contents") 20 | printf "%s" "$contents" > full.dat 21 | git add .gitattributes *.dat 22 | git commit -m "add files" | tee commit.log 23 | 24 | # cut from commit output 25 | # $ git cat-file -p main 26 | # tree 2d67d025fb1f9df9fa349412b4b130e982314e92 27 | tree="$(git cat-file -p main | cut -f 2 -d " " | head -n 1)" 28 | 29 | # cut from tree output 30 | # $ git cat-file -p "$tree" 31 | # 100644 blob 1e9f8f7cafb6af3a6f6ddf211fa39c45fccea7ab .gitattributes 32 | # 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 empty.dat 33 | # 100644 blob c5de5ac7dec1c40bafe60d24da9b498937640332 full.dat 34 | emptyblob="$(git cat-file -p "$tree" | cut -f 3 -d " " | grep "empty.dat" | cut -f 1 -d$'\t')" 35 | 36 | # look for lfs pointer in git blob 37 | [ 0 -eq "$(git cat-file -p "$emptyblob" | grep -c "lfs")" ] 38 | 39 | assert_pointer "main" "full.dat" "$contents_oid" 4 40 | 41 | git push origin main | tee push.log 42 | grep "Uploading LFS objects: 100% (1/1), 4 B" push.log 43 | ) 44 | end_test 45 | 46 | begin_test "pull zero len file" 47 | ( 48 | set -e 49 | 50 | clone_repo "$reponame" clone 51 | rm clone.log 52 | 53 | git status | grep -E "working (directory|tree) clean" 54 | ls -al 55 | 56 | if [ -s "empty.dat" ]; then 57 | echo "empty.dat has content:" 58 | cat empty.dat 59 | exit 1 60 | fi 61 | 62 | [ "full" = "$(cat full.dat)" ] 63 | ) 64 | end_test 65 | -------------------------------------------------------------------------------- /lfsapi/client.go: -------------------------------------------------------------------------------- 1 | package lfsapi 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | 7 | "github.com/git-lfs/git-lfs/v3/config" 8 | "github.com/git-lfs/git-lfs/v3/creds" 9 | "github.com/git-lfs/git-lfs/v3/lfshttp" 10 | ) 11 | 12 | func (c *Client) NewRequest(method string, e lfshttp.Endpoint, suffix string, body interface{}) (*http.Request, error) { 13 | return c.client.NewRequest(method, e, suffix, body) 14 | } 15 | 16 | // Do sends an HTTP request to get an HTTP response. It wraps net/http, adding 17 | // extra headers, redirection handling, and error reporting. 18 | func (c *Client) Do(req *http.Request) (*http.Response, error) { 19 | return c.client.Do(req) 20 | } 21 | 22 | // do performs an *http.Request respecting redirects, and handles the response 23 | // as defined in c.handleResponse. Notably, it does not alter the headers for 24 | // the request argument in any way. 25 | func (c *Client) do(req *http.Request, remote string, via []*http.Request) (*http.Response, error) { 26 | return c.client.Do(req) 27 | } 28 | 29 | func (c *Client) doWithAccess(req *http.Request, remote string, via []*http.Request, mode creds.AccessMode) (*http.Response, error) { 30 | return c.client.DoWithAccess(req, mode) 31 | } 32 | 33 | func (c *Client) LogRequest(r *http.Request, reqKey string) *http.Request { 34 | return c.client.LogRequest(r, reqKey) 35 | } 36 | 37 | func (c *Client) GitEnv() config.Environment { 38 | return c.client.GitEnv() 39 | } 40 | 41 | func (c *Client) OSEnv() config.Environment { 42 | return c.client.OSEnv() 43 | } 44 | 45 | func (c *Client) ConcurrentTransfers() int { 46 | return c.client.ConcurrentTransfers 47 | } 48 | 49 | func (c *Client) LogHTTPStats(w io.WriteCloser) { 50 | c.client.LogHTTPStats(w) 51 | } 52 | 53 | func (c *Client) Close() error { 54 | return c.client.Close() 55 | } 56 | -------------------------------------------------------------------------------- /commands/commands_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/git-lfs/git-lfs/v3/config" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var ( 11 | testcfg = config.NewFrom(config.Values{ 12 | Git: map[string][]string{ 13 | "lfs.fetchinclude": []string{"/default/include"}, 14 | "lfs.fetchexclude": []string{"/default/exclude"}, 15 | }, 16 | }) 17 | ) 18 | 19 | func TestDetermineIncludeExcludePathsReturnsCleanedPaths(t *testing.T) { 20 | inc := "/some/include" 21 | exc := "/some/exclude" 22 | i, e := determineIncludeExcludePaths(testcfg, &inc, &exc, true) 23 | 24 | assert.Equal(t, []string{"/some/include"}, i) 25 | assert.Equal(t, []string{"/some/exclude"}, e) 26 | } 27 | 28 | func TestDetermineIncludeExcludePathsReturnsEmptyPaths(t *testing.T) { 29 | inc := "" 30 | exc := "" 31 | i, e := determineIncludeExcludePaths(testcfg, &inc, &exc, true) 32 | 33 | assert.Empty(t, i) 34 | assert.Empty(t, e) 35 | } 36 | 37 | func TestDetermineIncludeExcludePathsReturnsDefaultsWhenAbsent(t *testing.T) { 38 | i, e := determineIncludeExcludePaths(testcfg, nil, nil, true) 39 | 40 | assert.Equal(t, []string{"/default/include"}, i) 41 | assert.Equal(t, []string{"/default/exclude"}, e) 42 | } 43 | 44 | func TestDetermineIncludeExcludePathsReturnsNothingWhenAbsent(t *testing.T) { 45 | i, e := determineIncludeExcludePaths(testcfg, nil, nil, false) 46 | 47 | assert.Empty(t, i) 48 | assert.Empty(t, e) 49 | } 50 | 51 | func TestSpecialGitRefsExclusion(t *testing.T) { 52 | assert.True(t, isSpecialGitRef("refs/notes/commits")) 53 | assert.True(t, isSpecialGitRef("refs/bisect/bad")) 54 | assert.True(t, isSpecialGitRef("refs/replace/abcdef90")) 55 | assert.True(t, isSpecialGitRef("refs/stash")) 56 | assert.False(t, isSpecialGitRef("refs/commits/abcdef90")) 57 | } 58 | -------------------------------------------------------------------------------- /script/hash-files: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "openssl" 4 | 5 | # This maps the OpenSSL name to the name used in the output file. 6 | # The order used is the order they should appear in the output file. 7 | DIGESTS = { 8 | 'BLAKE2b512' => 'BLAKE2b', 9 | 'BLAKE2s256' => 'BLAKE2s', 10 | 'SHA256' => 'SHA256', 11 | 'SHA384' => 'SHA384', 12 | 'SHA512' => 'SHA512', 13 | 'SHA512-256' => 'SHA512/256', 14 | 'SHA3-256' => 'SHA3-256', 15 | 'SHA3-384' => 'SHA3-384', 16 | 'SHA3-512' => 'SHA3-512', 17 | } 18 | 19 | class Hasher 20 | def initialize(file) 21 | @file = file 22 | @hashers = DIGESTS.map do |openssl, output| 23 | [output, OpenSSL::Digest.new(openssl)] 24 | end.to_h 25 | end 26 | 27 | def update(s) 28 | @hashers.values.each { |h| h.update(s) } 29 | end 30 | 31 | def to_a 32 | @hashers.map do |name, ctx| 33 | "#{name} (#{@file}) = #{ctx.digest.unpack("H*")[0]}\n" 34 | end.to_a 35 | end 36 | end 37 | 38 | unless RUBY_VERSION.split(".", 2)[0].to_i >= 3 39 | STDERR.puts "Ruby version must be at least 3.0" 40 | exit(1) 41 | end 42 | 43 | results = [] 44 | ARGV.each do |file| 45 | f = File.open(file) 46 | h = Hasher.new(file) 47 | while chunk = f.read(65536) do 48 | h.update(chunk) 49 | end 50 | results += h.to_a 51 | end 52 | 53 | # Sort entries first by order of algorithm name in DIGESTS, then by filename, 54 | # then print them. 55 | 56 | # Create a mapping of output name digest to order in the hash. 57 | names = DIGESTS.values.each_with_index.to_a.to_h 58 | results.sort_by do |s| 59 | # Split into digest name and remainder. The remainder starts with the 60 | # filename. 61 | pair = s.split(' ', 2).to_a 62 | # Order by the index of the digest and then the filename. 63 | [names[pair[0]], pair[1]] 64 | end.each { |l| puts l } 65 | -------------------------------------------------------------------------------- /errors/errors_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestChecksHandleGoErrors(t *testing.T) { 9 | err := errors.New("go error") 10 | 11 | if IsFatalError(err) { 12 | t.Error("go error should not be a fatal error") 13 | } 14 | } 15 | 16 | func TestCheckHandlesWrappedErrors(t *testing.T) { 17 | err := errors.New("go error") 18 | 19 | fatal := NewFatalError(err) 20 | 21 | if !IsFatalError(fatal) { 22 | t.Error("expected error to be fatal") 23 | } 24 | } 25 | 26 | func TestBehaviorWraps(t *testing.T) { 27 | err := errors.New("go error") 28 | 29 | fatal := NewFatalError(err) 30 | ni := NewNotImplementedError(fatal) 31 | 32 | if !IsNotImplementedError(ni) { 33 | t.Error("expected error to be not implemented") 34 | } 35 | 36 | if !IsFatalError(ni) { 37 | t.Error("expected wrapped error to also be fatal") 38 | } 39 | 40 | if IsNotImplementedError(fatal) { 41 | t.Error("expected fatal error to not be not implemented") 42 | } 43 | } 44 | 45 | func TestContextOnGoErrors(t *testing.T) { 46 | err := errors.New("go error") 47 | 48 | SetContext(err, "foo", "bar") 49 | 50 | v := GetContext(err, "foo") 51 | if v == "bar" { 52 | t.Error("expected empty context on go error") 53 | } 54 | } 55 | 56 | func TestContextOnWrappedErrors(t *testing.T) { 57 | err := NewFatalError(errors.New("go error")) 58 | 59 | SetContext(err, "foo", "bar") 60 | 61 | if v := GetContext(err, "foo"); v != "bar" { 62 | t.Error("expected to be able to use context on wrapped errors") 63 | } 64 | 65 | ctxt := Context(err) 66 | if ctxt["foo"] != "bar" { 67 | t.Error("expected to get the context of an error") 68 | } 69 | 70 | DelContext(err, "foo") 71 | 72 | if v := GetContext(err, "foo"); v == "bar" { 73 | t.Errorf("expected to delete from error context") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /t/t-ssh.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . "$(dirname "$0")/testlib.sh" 4 | 5 | begin_test "ssh with proxy command in lfs.url (default variant)" 6 | ( 7 | set -e 8 | 9 | reponame="batch-ssh-proxy-default" 10 | setup_remote_repo "$reponame" 11 | clone_repo "$reponame" "$reponame" 12 | 13 | sshurl="${GITSERVER/http:\/\//ssh://-oProxyCommand=ssh-proxy-test/}/$reponame" 14 | git config lfs.url "$sshurl" 15 | 16 | contents="test" 17 | oid="$(calc_oid "$contents")" 18 | git lfs track "*.dat" 19 | printf "%s" "$contents" > test.dat 20 | git add .gitattributes test.dat 21 | git commit -m "initial commit" 22 | 23 | unset GIT_SSH_VARIANT 24 | GIT_TRACE=1 git push origin main 2>&1 | tee push.log 25 | if [ "0" -eq "${PIPESTATUS[0]}" ]; then 26 | echo >&2 "fatal: push succeeded" 27 | exit 1 28 | fi 29 | 30 | grep 'expected.*git@127.0.0.1' push.log 31 | grep "lfs-ssh-echo -- -oProxyCommand" push.log 32 | ) 33 | end_test 34 | 35 | begin_test "ssh with proxy command in lfs.url (custom variant)" 36 | ( 37 | set -e 38 | 39 | reponame="batch-ssh-proxy-simple" 40 | setup_remote_repo "$reponame" 41 | clone_repo "$reponame" "$reponame" 42 | 43 | sshurl="${GITSERVER/http:\/\//ssh://-oProxyCommand=ssh-proxy-test/}/$reponame" 44 | git config lfs.url "$sshurl" 45 | 46 | contents="test" 47 | oid="$(calc_oid "$contents")" 48 | git lfs track "*.dat" 49 | printf "%s" "$contents" > test.dat 50 | git add .gitattributes test.dat 51 | git commit -m "initial commit" 52 | 53 | export GIT_SSH_VARIANT=simple 54 | GIT_TRACE=1 git push origin main 2>&1 | tee push.log 55 | if [ "0" -eq "${PIPESTATUS[0]}" ]; then 56 | echo >&2 "fatal: push succeeded" 57 | exit 1 58 | fi 59 | 60 | grep 'expected.*git@127.0.0.1' push.log 61 | grep "lfs-ssh-echo oProxyCommand" push.log 62 | ) 63 | end_test 64 | -------------------------------------------------------------------------------- /commands/command_post_commit.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/git-lfs/git-lfs/v3/git" 7 | "github.com/git-lfs/git-lfs/v3/tr" 8 | "github.com/rubyist/tracerx" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // postCommitCommand is run through Git's post-commit hook. The hook passes 13 | // no arguments. 14 | // This hook checks that files which are lockable and not locked are made read-only, 15 | // optimising that based on what was added / modified in the commit. 16 | // This is mainly to catch added files, since modified files should already be 17 | // locked. If we didn't do this, any added files would remain read/write on disk 18 | // even without a lock unless something else checked. 19 | func postCommitCommand(cmd *cobra.Command, args []string) { 20 | 21 | // Skip entire hook if lockable read only feature is disabled 22 | if !cfg.SetLockableFilesReadOnly() { 23 | os.Exit(0) 24 | } 25 | 26 | requireGitVersion() 27 | 28 | lockClient := newLockClient() 29 | 30 | // Skip this hook if no lockable patterns have been configured 31 | if len(lockClient.GetLockablePatterns()) == 0 { 32 | os.Exit(0) 33 | } 34 | 35 | tracerx.Printf("post-commit: checking file write flags at HEAD") 36 | // We can speed things up by looking at what changed in 37 | // HEAD, and only checking those lockable files 38 | files, err := git.GetFilesChanged("HEAD", "") 39 | 40 | if err != nil { 41 | LoggedError(err, tr.Tr.Get("Warning: post-commit failed: %v", err)) 42 | os.Exit(1) 43 | } 44 | tracerx.Printf("post-commit: checking write flags on %v", files) 45 | err = lockClient.FixLockableFileWriteFlags(files) 46 | if err != nil { 47 | LoggedError(err, tr.Tr.Get("Warning: post-commit locked file check failed: %v", err)) 48 | } 49 | 50 | } 51 | 52 | func init() { 53 | RegisterCommand("post-commit", postCommitCommand, nil) 54 | } 55 | -------------------------------------------------------------------------------- /tools/os_tools.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | 9 | "github.com/git-lfs/git-lfs/v3/subprocess" 10 | "github.com/git-lfs/git-lfs/v3/tr" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | func Getwd() (dir string, err error) { 15 | dir, err = os.Getwd() 16 | if err != nil { 17 | return 18 | } 19 | 20 | if isCygwin() { 21 | dir, err = translateCygwinPath(dir) 22 | if err != nil { 23 | return "", errors.Wrap(err, tr.Tr.Get("error converting working directory to Cygwin")) 24 | } 25 | } 26 | 27 | return 28 | } 29 | 30 | func translateCygwinPath(path string) (string, error) { 31 | cmd, err := subprocess.ExecCommand("cygpath", "-w", path) 32 | if err != nil { 33 | // If cygpath doesn't exist, that's okay: just return the paths 34 | // as we got it. 35 | return path, nil 36 | } 37 | // cygpath uses ISO-8850-1 as the default encoding if the locale is not 38 | // set, resulting in breakage, since we want a UTF-8 path. 39 | env := make([]string, 0, len(cmd.Env)+1) 40 | for _, val := range cmd.Env { 41 | if !strings.HasPrefix(val, "LC_ALL=") { 42 | env = append(env, val) 43 | } 44 | } 45 | cmd.Env = append(env, "LC_ALL=C.UTF-8") 46 | buf := &bytes.Buffer{} 47 | cmd.Stderr = buf 48 | out, err := cmd.Output() 49 | output := strings.TrimSpace(string(out)) 50 | if err != nil { 51 | // If cygpath doesn't exist, that's okay: just return the paths 52 | // as we got it. 53 | if _, ok := err.(*exec.Error); ok { 54 | return path, nil 55 | } 56 | return path, errors.New(tr.Tr.Get("failed to translate path from Cygwin to Windows: %s", buf.String())) 57 | } 58 | return output, nil 59 | } 60 | 61 | func TranslateCygwinPath(path string) (string, error) { 62 | if isCygwin() { 63 | var err error 64 | 65 | path, err = translateCygwinPath(path) 66 | if err != nil { 67 | return "", err 68 | } 69 | } 70 | return path, nil 71 | } 72 | -------------------------------------------------------------------------------- /lfs/gitscanner_catfilebatchcheckscanner_test.go: -------------------------------------------------------------------------------- 1 | package lfs 2 | 3 | import ( 4 | "bufio" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCatFileBatchCheckScannerWithValidOutput(t *testing.T) { 12 | lines := []string{ 13 | "short line", 14 | "0000000000000000000000000000000000000000 BLOB capitalized", 15 | "0000000000000000000000000000000000000001 blob not-a-size", 16 | "0000000000000000000000000000000000000002 blob 123", 17 | "0000000000000000000000000000000000000003 blob 1 0", 18 | "0000000000000000000000000000000000000004 blob 123456789", 19 | } 20 | r := strings.NewReader(strings.Join(lines, "\n")) 21 | s := &catFileBatchCheckScanner{ 22 | s: bufio.NewScanner(r), 23 | limit: 1024, 24 | } 25 | 26 | assertNextOID(t, s, "", "") 27 | assertNextOID(t, s, "", "") 28 | assertNextOID(t, s, "", "") 29 | assertNextOID(t, s, "0000000000000000000000000000000000000002", "") 30 | assertNextOID(t, s, "", "") 31 | assertNextOID(t, s, "", "0000000000000000000000000000000000000004") 32 | assertScannerDone(t, s) 33 | assert.Equal(t, "", s.LFSBlobOID()) 34 | assert.Equal(t, "", s.GitBlobOID()) 35 | } 36 | 37 | type stringScanner interface { 38 | Next() (string, bool, error) 39 | Err() error 40 | Scan() bool 41 | } 42 | 43 | type genericScanner interface { 44 | Err() error 45 | Scan() bool 46 | } 47 | 48 | func assertNextScan(t *testing.T, scanner genericScanner) { 49 | assert.True(t, scanner.Scan()) 50 | assert.Nil(t, scanner.Err()) 51 | } 52 | 53 | func assertNextOID(t *testing.T, scanner *catFileBatchCheckScanner, lfsBlobOID, gitBlobOID string) { 54 | assertNextScan(t, scanner) 55 | assert.Equal(t, lfsBlobOID, scanner.LFSBlobOID()) 56 | assert.Equal(t, gitBlobOID, scanner.GitBlobOID()) 57 | } 58 | 59 | func assertScannerDone(t *testing.T, scanner genericScanner) { 60 | assert.False(t, scanner.Scan()) 61 | assert.Nil(t, scanner.Err()) 62 | } 63 | -------------------------------------------------------------------------------- /t/cmd/lfstest-genrandom.go: -------------------------------------------------------------------------------- 1 | //go:build testtools 2 | // +build testtools 3 | 4 | package main 5 | 6 | import ( 7 | "crypto/rand" 8 | "encoding/base64" 9 | "encoding/binary" 10 | "fmt" 11 | "os" 12 | "strconv" 13 | ) 14 | 15 | const usageFmt = "Usage: %s [--base64|--base64url] []\n" 16 | 17 | func main() { 18 | offset := 1 19 | b64 := false 20 | b64url := false 21 | if len(os.Args) > offset && (os.Args[offset] == "--base64" || os.Args[offset] == "--base64url") { 22 | b64 = true 23 | b64url = os.Args[offset] == "--base64url" 24 | offset += 1 25 | } 26 | 27 | if len(os.Args) > offset+1 { 28 | fmt.Fprintf(os.Stderr, usageFmt, os.Args[0]) 29 | os.Exit(2) 30 | } 31 | 32 | var count uint64 = ^uint64(0) 33 | if len(os.Args) == offset+1 { 34 | var err error 35 | if count, err = strconv.ParseUint(os.Args[offset], 10, 64); err != nil { 36 | fmt.Fprintf(os.Stderr, "Error reading size: %s, %v\n", os.Args[offset], err) 37 | os.Exit(3) 38 | } 39 | } 40 | 41 | b := make([]byte, 32) 42 | bb := make([]byte, max(base64.RawStdEncoding.EncodedLen(len(b)), base64.RawURLEncoding.EncodedLen(len(b)))) 43 | for count > 0 { 44 | n, err := rand.Read(b) 45 | if err != nil { 46 | fmt.Fprintf(os.Stderr, "Error reading random bytes: %v\n", err) 47 | os.Exit(4) 48 | } 49 | if b64 { 50 | if b64url { 51 | base64.RawURLEncoding.Encode(bb, b[:n]) 52 | n = base64.RawURLEncoding.EncodedLen(n) 53 | } else { 54 | base64.RawStdEncoding.Encode(bb, b[:n]) 55 | n = base64.RawStdEncoding.EncodedLen(n) 56 | } 57 | } 58 | 59 | num := min(uint64(n), count) 60 | if b64 { 61 | err = binary.Write(os.Stdout, binary.LittleEndian, bb[:num]) 62 | } else { 63 | err = binary.Write(os.Stdout, binary.LittleEndian, b[:num]) 64 | } 65 | 66 | if err != nil { 67 | fmt.Fprintf(os.Stderr, "Error writing random bytes: %v\n", err) 68 | os.Exit(5) 69 | } 70 | count -= num 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /commands/command_untrack.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "strings" 7 | 8 | "github.com/git-lfs/git-lfs/v3/tools" 9 | "github.com/git-lfs/git-lfs/v3/tr" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // untrackCommand takes a list of paths as an argument, and removes each path from the 14 | // default attributes file (.gitattributes), if it exists. 15 | func untrackCommand(cmd *cobra.Command, args []string) { 16 | setupWorkingCopy() 17 | 18 | installHooks(false) 19 | 20 | if len(args) < 1 { 21 | Print("git lfs untrack [path]*") 22 | return 23 | } 24 | 25 | data, err := os.ReadFile(".gitattributes") 26 | if err != nil { 27 | return 28 | } 29 | 30 | attributes := strings.NewReader(string(data)) 31 | 32 | attributesFile, err := os.Create(".gitattributes") 33 | if err != nil { 34 | Print(tr.Tr.Get("Error opening '.gitattributes' for writing")) 35 | return 36 | } 37 | defer attributesFile.Close() 38 | 39 | scanner := bufio.NewScanner(attributes) 40 | 41 | // Iterate through each line of the attributes file and rewrite it, 42 | // if the path was meant to be untracked, omit it, and print a message instead. 43 | for scanner.Scan() { 44 | line := scanner.Text() 45 | if !strings.Contains(line, "filter=lfs") { 46 | attributesFile.WriteString(line + "\n") 47 | continue 48 | } 49 | 50 | path := strings.Fields(line)[0] 51 | if removePath(path, args) { 52 | Print(tr.Tr.Get("Untracking %q", unescapeAttrPattern(path))) 53 | } else { 54 | attributesFile.WriteString(line + "\n") 55 | } 56 | } 57 | } 58 | 59 | func removePath(path string, args []string) bool { 60 | withoutCurrentDir := tools.TrimCurrentPrefix(path) 61 | for _, t := range args { 62 | if withoutCurrentDir == escapeAttrPattern(tools.TrimCurrentPrefix(t)) { 63 | return true 64 | } 65 | } 66 | 67 | return false 68 | } 69 | 70 | func init() { 71 | RegisterCommand("untrack", untrackCommand, nil) 72 | } 73 | -------------------------------------------------------------------------------- /docs/man/git-lfs-filter-process.adoc: -------------------------------------------------------------------------------- 1 | = git-lfs-filter-process(1) 2 | 3 | == NAME 4 | 5 | git-lfs-filter-process - Git filter process that converts between pointer and actual content 6 | 7 | == SYNOPSIS 8 | 9 | `git lfs filter-process` + 10 | `git lfs filter-process --skip` 11 | 12 | == DESCRIPTION 13 | 14 | Implement the Git process filter API, exchanging handshake messages and 15 | then accepting and responding to requests to either clean or smudge a 16 | file. 17 | 18 | filter-process is always run by Git's filter process, and is configured 19 | by the repository's Git attributes. 20 | 21 | In your Git configuration or in a `.lfsconfig` file, you may set either 22 | or both of `lfs.fetchinclude` and `lfs.fetchexclude` to comma-separated 23 | lists of paths. If `lfs.fetchinclude` is defined, Git LFS pointer files 24 | will only be replaced with the contents of the corresponding Git LFS 25 | object file if their path matches one in that list, and if 26 | `lfs.fetchexclude` is defined, Git LFS pointer files will only be 27 | replaced with the contents of the corresponding Git LFS object file if 28 | their path does not match one in that list. Paths are matched using 29 | wildcard matching as per gitignore(5). Git LFS pointer files that are 30 | not replaced with the contents of their corresponding object files are 31 | simply copied to standard output without change. 32 | 33 | The filter process uses Git's pkt-line protocol to communicate, and is 34 | documented in detail in gitattributes(5). 35 | 36 | == OPTIONS 37 | 38 | Without any options, filter-process accepts and responds to requests 39 | normally. 40 | 41 | `-s`:: 42 | `--skip`:: 43 | Skip automatic downloading of objects on clone or pull. 44 | `GIT_LFS_SKIP_SMUDGE`:: 45 | Disables the smudging process. For more, see: git-lfs-config(5). 46 | 47 | == SEE ALSO 48 | 49 | git-lfs-clean(1), git-lfs-install(1), git-lfs-smudge(1), 50 | gitattributes(5), gitignore(5). 51 | 52 | Part of the git-lfs(1) suite. 53 | --------------------------------------------------------------------------------