From 133aa297c69411be08c2628207ee688876dd6607 Mon Sep 17 00:00:00 2001 From: YuxuanLiuTier4Desktop <619684051@qq.com> Date: Wed, 17 Dec 2025 12:04:26 +0900 Subject: [PATCH 1/7] feat: blobless clone Signed-off-by: YuxuanLiuTier4Desktop <619684051@qq.com> --- vcs2l/clients/git.py | 7 ++++++- vcs2l/commands/import_.py | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/vcs2l/clients/git.py b/vcs2l/clients/git.py index b8022a3..0090f9b 100644 --- a/vcs2l/clients/git.py +++ b/vcs2l/clients/git.py @@ -326,6 +326,8 @@ def import_(self, command): # fetch updates for existing repo cmd_fetch = [GitClient._executable, 'fetch', remote] + if command.blobless_clone: + cmd_fetch.append('--filter=blob:none') if command.shallow: result_version_type, version_name = self._check_version_type( command.url, checkout_version, command.retry @@ -409,7 +411,10 @@ def import_(self, command): if not command.shallow or version_type in (None, 'branch'): cmd_clone = [GitClient._executable, 'clone', command.url, '.'] - if version_type == 'branch': + if command.blobless_clone: + cmd_clone += ['--filter=blob:none', '--no-checkout'] + checkout_version = command.version + elif version_type == 'branch': cmd_clone += ['-b', version_name] checkout_version = None else: diff --git a/vcs2l/commands/import_.py b/vcs2l/commands/import_.py index 54af290..c3c3dc9 100644 --- a/vcs2l/commands/import_.py +++ b/vcs2l/commands/import_.py @@ -28,6 +28,7 @@ def __init__(self, args, url, version=None, recursive=False, shallow=False): self.skip_existing = args.skip_existing self.recursive = recursive self.shallow = shallow + self.blobless_clone = args.blobless_clone def get_parser(): @@ -75,6 +76,12 @@ def get_parser(): help="Don't overwrite existing directories or change custom checkouts " 'in repos using the same URL (but fetch repos with same URL)', ) + group.add_argument( + '--blobless-clone', + action='store_true', + default=False, + help='Only clone the commit history first, then checkout to the target version to obtain files', + ) return parser From 4ea438fe288cf81d62a6fe224ea32aac5b3aba8d Mon Sep 17 00:00:00 2001 From: Yuxuan Liu <619684051@qq.com> Date: Tue, 24 Feb 2026 13:00:42 +0900 Subject: [PATCH 2/7] feat: explicitly added blobless clone in constructor Signed-off-by: Yuxuan Liu <619684051@qq.com> --- vcs2l/commands/import_.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/vcs2l/commands/import_.py b/vcs2l/commands/import_.py index c3c3dc9..cbf8cb6 100644 --- a/vcs2l/commands/import_.py +++ b/vcs2l/commands/import_.py @@ -19,7 +19,15 @@ class ImportCommand(Command): command = 'import' help = 'Import the list of repositories' - def __init__(self, args, url, version=None, recursive=False, shallow=False): + def __init__( + self, + args, + url, + version=None, + recursive=False, + shallow=False, + blobless_clone=False, + ): super(ImportCommand, self).__init__(args) self.url = url self.version = version @@ -28,7 +36,7 @@ def __init__(self, args, url, version=None, recursive=False, shallow=False): self.skip_existing = args.skip_existing self.recursive = recursive self.shallow = shallow - self.blobless_clone = args.blobless_clone + self.blobless_clone = blobless_clone def get_parser(): @@ -214,6 +222,7 @@ def generate_jobs(repos, args): str(repo['version']) if 'version' in repo else None, recursive=args.recursive, shallow=args.shallow, + blobless_clone=args.blobless_clone, ) job = {'client': client, 'command': command} jobs.append(job) From 9ae0833d3f8152b9b11680c4095567c05de8d4e2 Mon Sep 17 00:00:00 2001 From: Yuxuan Liu <619684051@qq.com> Date: Tue, 24 Feb 2026 13:01:42 +0900 Subject: [PATCH 3/7] feat: shallow & blobless clone mutually exclusive check Signed-off-by: Yuxuan Liu <619684051@qq.com> --- vcs2l/commands/import_.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/vcs2l/commands/import_.py b/vcs2l/commands/import_.py index cbf8cb6..4265f6e 100644 --- a/vcs2l/commands/import_.py +++ b/vcs2l/commands/import_.py @@ -263,6 +263,16 @@ def main(args=None, stdout=None, stderr=None): except (RuntimeError, request.URLError) as e: print(ansi('redf') + str(e) + ansi('reset'), file=sys.stderr) return 1 + + # check if both '--shallow' and '--blobless-clone' options are used, since they are mutually exclusive + if args.shallow and args.blobless_clone: + print( + ansi('redf') + + "'--shallow' and '--blobless-clone' are mutually exclusive options" + + ansi('reset'), + file=sys.stderr, + ) + return 1 jobs = generate_jobs(repos, args) add_dependencies(jobs) From a15ab81612f1fe26947a68d5c9f96b1b69698070 Mon Sep 17 00:00:00 2001 From: Yuxuan Liu <619684051@qq.com> Date: Tue, 24 Feb 2026 13:06:05 +0900 Subject: [PATCH 4/7] feat: handle branch and version correctly in blobless clone Signed-off-by: Yuxuan Liu <619684051@qq.com> --- vcs2l/clients/git.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/vcs2l/clients/git.py b/vcs2l/clients/git.py index 0090f9b..6c9ea77 100644 --- a/vcs2l/clients/git.py +++ b/vcs2l/clients/git.py @@ -412,8 +412,14 @@ def import_(self, command): if not command.shallow or version_type in (None, 'branch'): cmd_clone = [GitClient._executable, 'clone', command.url, '.'] if command.blobless_clone: - cmd_clone += ['--filter=blob:none', '--no-checkout'] - checkout_version = command.version + cmd_clone += ['--filter=blob:none'] + if version_type == 'branch': + cmd_clone += ['-b', version_name] + elif command.version: + cmd_clone.append('--no-checkout') + checkout_version = command.version + else: + checkout_version = None elif version_type == 'branch': cmd_clone += ['-b', version_name] checkout_version = None From 46bc9ce6ec06d484cd3a4fbde821ccd745426736 Mon Sep 17 00:00:00 2001 From: Yuxuan Liu <619684051@qq.com> Date: Tue, 24 Feb 2026 13:32:53 +0900 Subject: [PATCH 5/7] feat: add test and correctly handle checked_version Signed-off-by: Yuxuan Liu <619684051@qq.com> --- test/import_blobless.txt | 53 ++++++++++++++ ...rt_blobless_shallow_mutually_exclusive.txt | 1 + test/test_commands.py | 72 +++++++++++++++++++ vcs2l/clients/git.py | 1 + 4 files changed, 127 insertions(+) create mode 100644 test/import_blobless.txt create mode 100644 test/import_blobless_shallow_mutually_exclusive.txt diff --git a/test/import_blobless.txt b/test/import_blobless.txt new file mode 100644 index 0000000..b461059 --- /dev/null +++ b/test/import_blobless.txt @@ -0,0 +1,53 @@ +...... +=== ./immutable/hash (git) === +Cloning into '.'... +warning: filtering not recognized by server, ignoring +Note: switching to '5b3504594f7354121cf024dc734bf79e270cffd3'. + +You are in 'detached HEAD' state. You can look around, make experimental +changes and commit them, and you can discard any commits you make in this +state without impacting any branches by switching back to a branch. + +If you want to create a new branch to retain commits you create, you may +do so (now or later) by using -c with the switch command. Example: + + git switch -c + +Or undo this operation with: + + git switch - + +Turn off this advice by setting config variable advice.detachedHead to false + +HEAD is now at 5b35045... update changelog +=== ./immutable/hash_tar (tar) === +Downloaded tarball from 'file:///vcstmp/archive.tar.gz' and unpacked it +=== ./immutable/hash_zip (zip) === +Downloaded zipfile from 'file:///vcstmp/archive.zip' and unpacked it +=== ./immutable/tag (git) === +Cloning into '.'... +warning: filtering not recognized by server, ignoring +Note: switching to 'tags/0.1.27'. + +You are in 'detached HEAD' state. You can look around, make experimental +changes and commit them, and you can discard any commits you make in this +state without impacting any branches by switching back to a branch. + +If you want to create a new branch to retain commits you create, you may +do so (now or later) by using -c with the switch command. Example: + + git switch -c + +Or undo this operation with: + + git switch - + +Turn off this advice by setting config variable advice.detachedHead to false + +HEAD is now at 8087b72... 0.1.27 +=== ./vcs2l (git) === +Cloning into '.'... +warning: filtering not recognized by server, ignoring +=== ./without_version (git) === +Cloning into '.'... +warning: filtering not recognized by server, ignoring diff --git a/test/import_blobless_shallow_mutually_exclusive.txt b/test/import_blobless_shallow_mutually_exclusive.txt new file mode 100644 index 0000000..7d40b8c --- /dev/null +++ b/test/import_blobless_shallow_mutually_exclusive.txt @@ -0,0 +1 @@ +'--shallow' and '--blobless-clone' are mutually exclusive options diff --git a/test/test_commands.py b/test/test_commands.py index 151deef..22c13ca 100644 --- a/test/test_commands.py +++ b/test/test_commands.py @@ -270,6 +270,78 @@ def test_import_shallow(self): finally: rmtree(workdir) + def test_import_blobless(self): + workdir = os.path.join(TEST_WORKSPACE, 'import-blobless') + os.makedirs(workdir) + try: + output = run_command( + 'import', + ['--blobless-clone', '--input', self.repos_file_path, '.'], + subfolder='import-blobless', + ) + # the actual output contains absolute paths + output = output.replace( + b'repository in ' + workdir.encode() + b'/', b'repository in ./' + ) + expected = get_expected_output('import_blobless') + # newer git versions don't append ... after the commit hash + assert output == expected or output == expected.replace(b'... ', b' ') + + git_repos = [ + os.path.join(workdir, 'immutable', 'hash'), + os.path.join(workdir, 'immutable', 'tag'), + os.path.join(workdir, 'vcs2l'), + os.path.join(workdir, 'without_version'), + ] + for repo_path in git_repos: + partial_filter = subprocess.check_output( + ['git', 'config', '--get', 'remote.origin.partialclonefilter'], + stderr=subprocess.STDOUT, + cwd=repo_path, + ).strip() + self.assertEqual(partial_filter, b'blob:none') + promisor = subprocess.check_output( + ['git', 'config', '--get', 'remote.origin.promisor'], + stderr=subprocess.STDOUT, + cwd=repo_path, + ).strip() + self.assertEqual(promisor, b'true') + finally: + rmtree(workdir) + + def test_import_blobless_shallow_mutually_exclusive(self): + repo_root = os.path.dirname(os.path.dirname(__file__)) + script = os.path.join(repo_root, 'scripts', 'vcs-import') + env = dict(os.environ) + env.update( + GIT_CONFIG_GLOBAL=os.path.join(repo_root, 'test', '.gitconfig'), + LANG='en_US.UTF-8', + PYTHONPATH=repo_root + os.pathsep + env.get('PYTHONPATH', ''), + PYTHONWARNINGS='ignore', + ) + try: + subprocess.check_output( + [ + sys.executable, + script, + '--shallow', + '--blobless-clone', + '--input', + self.repos_file_path, + '.', + ], + stderr=subprocess.STDOUT, + cwd=TEST_WORKSPACE, + env=env, + ) + except subprocess.CalledProcessError as e: + output = adapt_command_output(e.output, TEST_WORKSPACE) + else: + self.fail("Expected '--shallow' and '--blobless-clone' to be rejected") + + expected = get_expected_output('import_blobless_shallow_mutually_exclusive') + self.assertEqual(output, expected) + def test_import_url(self): workdir = os.path.join(TEST_WORKSPACE, 'import-url') os.makedirs(workdir) diff --git a/vcs2l/clients/git.py b/vcs2l/clients/git.py index 6c9ea77..ffbea5a 100644 --- a/vcs2l/clients/git.py +++ b/vcs2l/clients/git.py @@ -415,6 +415,7 @@ def import_(self, command): cmd_clone += ['--filter=blob:none'] if version_type == 'branch': cmd_clone += ['-b', version_name] + checkout_version = None elif command.version: cmd_clone.append('--no-checkout') checkout_version = command.version From 96f7433b556cf37235c7171e6a321cd005d628c8 Mon Sep 17 00:00:00 2001 From: Yuxuan Liu <619684051@qq.com> Date: Tue, 24 Feb 2026 13:42:01 +0900 Subject: [PATCH 6/7] fix: fix not needed env Signed-off-by: Yuxuan Liu <619684051@qq.com> --- test/test_commands.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/test_commands.py b/test/test_commands.py index 22c13ca..09b57b2 100644 --- a/test/test_commands.py +++ b/test/test_commands.py @@ -312,13 +312,6 @@ def test_import_blobless(self): def test_import_blobless_shallow_mutually_exclusive(self): repo_root = os.path.dirname(os.path.dirname(__file__)) script = os.path.join(repo_root, 'scripts', 'vcs-import') - env = dict(os.environ) - env.update( - GIT_CONFIG_GLOBAL=os.path.join(repo_root, 'test', '.gitconfig'), - LANG='en_US.UTF-8', - PYTHONPATH=repo_root + os.pathsep + env.get('PYTHONPATH', ''), - PYTHONWARNINGS='ignore', - ) try: subprocess.check_output( [ @@ -332,7 +325,6 @@ def test_import_blobless_shallow_mutually_exclusive(self): ], stderr=subprocess.STDOUT, cwd=TEST_WORKSPACE, - env=env, ) except subprocess.CalledProcessError as e: output = adapt_command_output(e.output, TEST_WORKSPACE) From 0f3d044b7d0c532311da2eb3835561333ef94019 Mon Sep 17 00:00:00 2001 From: Yuxuan Liu <619684051@qq.com> Date: Mon, 2 Mar 2026 09:31:30 +0900 Subject: [PATCH 7/7] feat: use mutually exclusive group instead of hard-coded on Signed-off-by: Yuxuan Liu <619684051@qq.com> --- ...rt_blobless_shallow_mutually_exclusive.txt | 1 - test/test_commands.py | 25 ------------------- vcs2l/commands/import_.py | 24 ++++++------------ 3 files changed, 8 insertions(+), 42 deletions(-) delete mode 100644 test/import_blobless_shallow_mutually_exclusive.txt diff --git a/test/import_blobless_shallow_mutually_exclusive.txt b/test/import_blobless_shallow_mutually_exclusive.txt deleted file mode 100644 index 7d40b8c..0000000 --- a/test/import_blobless_shallow_mutually_exclusive.txt +++ /dev/null @@ -1 +0,0 @@ -'--shallow' and '--blobless-clone' are mutually exclusive options diff --git a/test/test_commands.py b/test/test_commands.py index 09b57b2..249ce00 100644 --- a/test/test_commands.py +++ b/test/test_commands.py @@ -309,31 +309,6 @@ def test_import_blobless(self): finally: rmtree(workdir) - def test_import_blobless_shallow_mutually_exclusive(self): - repo_root = os.path.dirname(os.path.dirname(__file__)) - script = os.path.join(repo_root, 'scripts', 'vcs-import') - try: - subprocess.check_output( - [ - sys.executable, - script, - '--shallow', - '--blobless-clone', - '--input', - self.repos_file_path, - '.', - ], - stderr=subprocess.STDOUT, - cwd=TEST_WORKSPACE, - ) - except subprocess.CalledProcessError as e: - output = adapt_command_output(e.output, TEST_WORKSPACE) - else: - self.fail("Expected '--shallow' and '--blobless-clone' to be rejected") - - expected = get_expected_output('import_blobless_shallow_mutually_exclusive') - self.assertEqual(output, expected) - def test_import_url(self): workdir = os.path.join(TEST_WORKSPACE, 'import-url') os.makedirs(workdir) diff --git a/vcs2l/commands/import_.py b/vcs2l/commands/import_.py index 4265f6e..2c19e47 100644 --- a/vcs2l/commands/import_.py +++ b/vcs2l/commands/import_.py @@ -58,12 +58,19 @@ def get_parser(): help="Delete existing directories if they don't contain the " 'repository being imported', ) - group.add_argument( + clone_type_group = group.add_mutually_exclusive_group() + clone_type_group.add_argument( '--shallow', action='store_true', default=False, help='Create a shallow clone without a history', ) + clone_type_group.add_argument( + '--blobless-clone', + action='store_true', + default=False, + help='Only clone the commit history first, then checkout to the target version to obtain files', + ) group.add_argument( '--recursive', action='store_true', @@ -84,12 +91,6 @@ def get_parser(): help="Don't overwrite existing directories or change custom checkouts " 'in repos using the same URL (but fetch repos with same URL)', ) - group.add_argument( - '--blobless-clone', - action='store_true', - default=False, - help='Only clone the commit history first, then checkout to the target version to obtain files', - ) return parser @@ -264,15 +265,6 @@ def main(args=None, stdout=None, stderr=None): print(ansi('redf') + str(e) + ansi('reset'), file=sys.stderr) return 1 - # check if both '--shallow' and '--blobless-clone' options are used, since they are mutually exclusive - if args.shallow and args.blobless_clone: - print( - ansi('redf') - + "'--shallow' and '--blobless-clone' are mutually exclusive options" - + ansi('reset'), - file=sys.stderr, - ) - return 1 jobs = generate_jobs(repos, args) add_dependencies(jobs)