-
Notifications
You must be signed in to change notification settings - Fork 119
SG-44731 Adopt Git LFS for vendored dependency ZIPs #1128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
9c5063f
4c81128
d17ca35
2c3863d
48e3f31
154bb87
fe8bbd3
7367e16
a1e1694
60055cf
6e6185e
5b5ec11
81d268d
4567992
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| # By accessing, using, copying or modifying this work you indicate your | ||
| # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights | ||
| # not expressly granted therein are reserved by Shotgun Software Inc. | ||
| import json | ||
| import os | ||
| import subprocess | ||
| import tempfile | ||
|
|
@@ -202,9 +203,58 @@ | |
| ) | ||
| log.debug("Execution successful. stderr/stdout: '%s'" % output) | ||
|
|
||
| self._validate_lfs_content(target_path) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just thinking out loud, not a real request from my side. What if we validate the lfs content earlier to prevent doing a lot of steps that will end up meaningless when LFS is not installed? That way we can warn the user in advance. I'm not sure if that will be possible to be honest.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not possible unfortunately. We first need to clone the repo before indentifying if it's using LFS. |
||
|
|
||
| # return the last returned stdout/stderr | ||
| return output | ||
|
|
||
| def _validate_lfs_content(self, repo_path: str) -> None: | ||
| """ | ||
| Checks that Git LFS tracked files in the checked out repo were | ||
| actually resolved to their real content, rather than left as | ||
| literal pointer text. This happens silently when git-lfs isn't | ||
| installed/registered on this machine - git checks out the pointer | ||
| text with no error of its own. | ||
|
|
||
| :param repo_path: path to a checked out git repository. | ||
| :raises TankGitError: if the repo uses Git LFS but git-lfs isn't | ||
| available, or some LFS content wasn't downloaded. | ||
| """ | ||
|
julien-lang marked this conversation as resolved.
|
||
| gitattributes_path = os.path.join(repo_path, ".gitattributes") | ||
| try: | ||
| with open(gitattributes_path, "r") as fh: | ||
| uses_lfs = "filter=lfs" in fh.read() | ||
|
julien-lang marked this conversation as resolved.
julien-lang marked this conversation as resolved.
|
||
| except OSError: | ||
| uses_lfs = False | ||
|
|
||
| if not uses_lfs: | ||
| return | ||
|
|
||
| try: | ||
| output = _check_output( | ||
| "git lfs ls-files --json", | ||
| cwd=repo_path, | ||
| shell=True, | ||
| ) | ||
|
Check warning on line 238 in python/tank/descriptor/io_descriptor/git.py
|
||
| except SubprocessCalledProcessError as err: | ||
| raise TankGitError( | ||
| f"{self} uses Git LFS to store some of its files, but git-lfs " | ||
| "does not appear to be installed on this machine. Install " | ||
| "it from https://git-lfs.com, run `git lfs install`, and " | ||
| "try again." | ||
| ) from err | ||
|
|
||
| files = json.loads(output).get("files") or [] | ||
| missing = [f["name"] for f in files if not f.get("checkout")] | ||
| if missing: | ||
|
julien-lang marked this conversation as resolved.
|
||
| raise TankGitError( | ||
| "Git LFS content for the following file(s) in %s was not " | ||
| "downloaded correctly - they still contain pointer text " | ||
| "instead of their real content: %s. Make sure git-lfs is " | ||
| "installed (https://git-lfs.com) and run `git lfs install`, " | ||
| "then try again." % (self, ", ".join(missing)) | ||
| ) | ||
|
|
||
| def _tmp_clone_then_execute_git_commands(self, commands, depth=None, ref=None): | ||
| """ | ||
| Clone into a temp location and executes the given | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| # Copyright (c) 2026 Shotgun Software Inc. | ||
| # | ||
| # CONFIDENTIAL AND PROPRIETARY | ||
| # | ||
| # This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit | ||
| # Source Code License included in this distribution package. See LICENSE. | ||
| # By accessing, using, copying or modifying this work you indicate your | ||
| # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights | ||
| # not expressly granted therein are reserved by Shotgun Software Inc. | ||
|
|
||
| import os | ||
| import shutil | ||
| import subprocess | ||
| import tempfile | ||
| import unittest.mock | ||
|
|
||
| import sgtk | ||
| from tank_test.tank_test_base import ( | ||
| ShotgunTestBase, | ||
| _is_git_lfs_missing, | ||
| _is_git_missing, | ||
| setUpModule, # noqa | ||
| skip_if_git_lfs_missing, | ||
| skip_if_git_missing, | ||
| ) | ||
|
|
||
| LFS_FILE_NAME = "sample.dat" | ||
| LFS_FILE_CONTENT = "hello lfs content for tk-core tests\n" | ||
|
|
||
|
|
||
| @skip_if_git_missing | ||
| @skip_if_git_lfs_missing | ||
| class TestGitLFSIODescriptor(ShotgunTestBase): | ||
| """ | ||
| Testing the Git LFS validation performed by IODescriptorGit. | ||
| """ | ||
|
|
||
| @classmethod | ||
| def setUpClass(cls): | ||
| """ | ||
| Builds, once for the whole test class, a small local git repo with a | ||
| single file tracked via Git LFS. This repo is used as a read-only | ||
| clone source by every test - no need to check in a repo fixture, | ||
| it's trivial and fast to (re)build on demand. | ||
| """ | ||
| super().setUpClass() | ||
|
|
||
| cls.git_lfs_repo_uri = None | ||
| if _is_git_missing() or _is_git_lfs_missing(): | ||
| # tests are skipped in this case, no need to build the repo | ||
| return | ||
|
|
||
| cls.git_lfs_repo_uri = tempfile.mkdtemp(prefix="tk_test_lfs_repo_") | ||
| env = dict( | ||
| os.environ, | ||
| GIT_AUTHOR_NAME="tk-core tests", | ||
| GIT_AUTHOR_EMAIL="tk-core-tests@example.com", | ||
| GIT_COMMITTER_NAME="tk-core tests", | ||
| GIT_COMMITTER_EMAIL="tk-core-tests@example.com", | ||
| ) | ||
|
|
||
| def _run(*args): | ||
| subprocess.check_call(args, cwd=cls.git_lfs_repo_uri, env=env) | ||
|
|
||
| _run("git", "init", "-q", "-b", "master") | ||
|
julien-lang marked this conversation as resolved.
|
||
| _run("git", "lfs", "install", "--local") | ||
|
julien-lang marked this conversation as resolved.
|
||
| with open(os.path.join(cls.git_lfs_repo_uri, LFS_FILE_NAME), "w") as fh: | ||
| fh.write(LFS_FILE_CONTENT) | ||
| _run("git", "lfs", "track", LFS_FILE_NAME) | ||
| _run("git", "add", ".gitattributes", LFS_FILE_NAME) | ||
| _run("git", "commit", "-q", "-m", "initial commit with an lfs file") | ||
|
|
||
| @classmethod | ||
| def tearDownClass(cls): | ||
| if cls.git_lfs_repo_uri: | ||
| shutil.rmtree(cls.git_lfs_repo_uri, ignore_errors=True) | ||
| super().tearDownClass() | ||
|
|
||
| def setUp(self): | ||
| """ | ||
| Sets up the next test's environment. | ||
| """ | ||
| ShotgunTestBase.setUp(self) | ||
|
|
||
| # each test gets its own bundle cache so a download in one test can't | ||
| # be mistaken for an already-resolved download in another | ||
| self.bundle_cache = os.path.join( | ||
| self.project_root, "bundle_cache_%s" % self._testMethodName | ||
| ) | ||
|
|
||
| def _create_desc( | ||
| self, | ||
| location, | ||
| resolve_latest=False, | ||
| desc_type=sgtk.descriptor.Descriptor.CONFIG, | ||
| ): | ||
| """ | ||
| Helper method around create_descriptor | ||
| """ | ||
| return sgtk.descriptor.create_descriptor( | ||
| self.mockgun, | ||
| desc_type, | ||
| location, | ||
| bundle_cache_root_override=self.bundle_cache, | ||
| resolve_latest=resolve_latest, | ||
| ) | ||
|
|
||
| def test_lfs_content_resolved(self): | ||
| """ | ||
| A repo whose Git LFS content resolves normally should check out fine. | ||
| """ | ||
| location_dict = { | ||
| "type": "git_branch", | ||
| "path": self.git_lfs_repo_uri, | ||
| "branch": "master", | ||
| } | ||
|
|
||
| desc = self._create_desc(location_dict, True) | ||
| desc.ensure_local() | ||
|
|
||
| lfs_file_path = os.path.join(desc.get_path(), LFS_FILE_NAME) | ||
| with open(lfs_file_path, "r") as fh: | ||
| self.assertEqual(fh.read(), LFS_FILE_CONTENT) | ||
|
|
||
| def test_lfs_content_unresolved(self): | ||
| """ | ||
| If Git LFS content is checked out as unresolved pointer text (e.g. | ||
| git-lfs wasn't registered on the machine that did the clone), Toolkit | ||
| should raise rather than silently use the pointer file as-is. | ||
| """ | ||
| location_dict = { | ||
| "type": "git_branch", | ||
| "path": self.git_lfs_repo_uri, | ||
| "branch": "master", | ||
| } | ||
|
|
||
| desc = self._create_desc(location_dict, True) | ||
|
|
||
| with unittest.mock.patch.dict(os.environ, {"GIT_LFS_SKIP_SMUDGE": "1"}): | ||
| with self.assertRaises(sgtk.descriptor.errors.TankDescriptorError): | ||
| desc.ensure_local() | ||
Uh oh!
There was an error while loading. Please reload this page.