From 3ab3f6a90254dc704009602a7242c09ebc3e1737 Mon Sep 17 00:00:00 2001 From: Miloud Belarebia <136994453+miloudbelarebia@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:20:53 +0200 Subject: [PATCH] fetch: add --num option to fetch the last N commits Mirrors `dvc gc --num`: `dvc fetch --num N` fetches the cache for the last N commits. On its own it walks back from the current branch's HEAD; combined with --all-branches it applies to each branch tip. Closes #11033 --- dvc/commands/data_sync.py | 15 +++++++++++++- dvc/repo/fetch.py | 13 +++++++++++- tests/func/test_data_cloud.py | 30 ++++++++++++++++++++++++++++ tests/unit/command/test_data_sync.py | 3 +++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/dvc/commands/data_sync.py b/dvc/commands/data_sync.py index 8f46e68211..57ede4df7f 100644 --- a/dvc/commands/data_sync.py +++ b/dvc/commands/data_sync.py @@ -93,6 +93,7 @@ def run(self): all_branches=self.args.all_branches, all_tags=self.args.all_tags, all_commits=self.args.all_commits, + num=self.args.num, with_deps=self.args.with_deps, recursive=self.args.recursive, run_cache=self.args.run_cache, @@ -135,7 +136,7 @@ def shared_parent_parser(): return parent_parser -def add_parser(subparsers, _parent_parser): +def add_parser(subparsers, _parent_parser): # noqa: PLR0915 from dvc.commands.status import CmdDataStatus # Pull @@ -309,6 +310,18 @@ def add_parser(subparsers, _parent_parser): default=False, help="Fetch cache for all commits.", ) + fetch_parser.add_argument( + "--num", + type=int, + default=1, + dest="num", + metavar="", + help=( + "Fetch cache for the last `num` commits. Defaults to the current " + "branch when used on its own, or applies to each branch with " + "`--all-branches`." + ), + ) fetch_parser.add_argument( "-d", "--with-deps", diff --git a/dvc/repo/fetch.py b/dvc/repo/fetch.py index ee11ec88d8..cc218c207a 100644 --- a/dvc/repo/fetch.py +++ b/dvc/repo/fetch.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from dvc.exceptions import DownloadError +from dvc.exceptions import DownloadError, InvalidArgumentError from dvc.log import logger from dvc.stage.cache import RunCacheNotSupported from dvc.ui import ui @@ -35,6 +35,7 @@ def _collect_indexes( # noqa: PLR0913 recursive=False, all_commits=False, revs=None, + num=1, workspace=True, max_size=None, types=None, @@ -66,6 +67,7 @@ def outs_filter(out: "Output") -> bool: all_branches=all_branches, all_tags=all_tags, all_commits=all_commits, + num=num, workspace=workspace, ): try: @@ -110,6 +112,7 @@ def fetch( # noqa: PLR0913 all_commits=False, run_cache=False, revs=None, + num=1, workspace=True, max_size=None, types=None, @@ -136,6 +139,13 @@ def fetch( # noqa: PLR0913 if isinstance(targets, str): targets = [targets] + if num < 1: + raise InvalidArgumentError("`--num` must be a positive integer.") + + if num > 1 and not (revs or all_branches or all_tags or all_commits): + # `--num` on its own fetches the last `num` commits of the current branch. + revs = ["HEAD"] + failed_count = 0 transferred_count = 0 @@ -157,6 +167,7 @@ def fetch( # noqa: PLR0913 recursive=recursive, all_commits=all_commits, revs=revs, + num=num, workspace=workspace, max_size=max_size, types=types, diff --git a/tests/func/test_data_cloud.py b/tests/func/test_data_cloud.py index 8e4cfdd7e1..8f38be2d06 100644 --- a/tests/func/test_data_cloud.py +++ b/tests/func/test_data_cloud.py @@ -537,6 +537,36 @@ def test_push_pull_all(tmp_dir, scm, dvc, local_remote, key, expected): } +def test_fetch_num(tmp_dir, scm, dvc, local_remote): + # Three commits, each tracking a distinct version (hash) of the same file. + tmp_dir.dvc_gen("foo", "first", commit="first") + tmp_dir.dvc_gen("foo", "second", commit="second") + tmp_dir.dvc_gen("foo", "third", commit="third") + + assert dvc.push(all_commits=True) == 3 + + # `--num` on its own fetches the last `num` commits of the current branch: + # HEAD ("third") and HEAD~1 ("second"), but not HEAD~2 ("first"). + clean(["foo"], dvc) + assert dvc.fetch(num=2) == 2 + + # default (`num=1`) only fetches the workspace revision ("third"). + clean(["foo"], dvc) + assert dvc.fetch() == 1 + + # `--num` larger than history is capped at the available commits. + clean(["foo"], dvc) + assert dvc.fetch(num=10) == 3 + + +def test_fetch_num_invalid(tmp_dir, dvc, local_remote): + from dvc.exceptions import InvalidArgumentError + + tmp_dir.dvc_gen({"foo": "foo"}) + with pytest.raises(InvalidArgumentError): + dvc.fetch(num=0) + + def test_push_pull_fetch_pipeline_stages(tmp_dir, dvc, run_copy, local_remote): tmp_dir.dvc_gen("foo", "foo") run_copy("foo", "bar", name="copy-foo-bar") diff --git a/tests/unit/command/test_data_sync.py b/tests/unit/command/test_data_sync.py index 13f180c88f..b4743809ab 100644 --- a/tests/unit/command/test_data_sync.py +++ b/tests/unit/command/test_data_sync.py @@ -15,6 +15,8 @@ def test_fetch(mocker, dvc): "--all-branches", "--all-tags", "--all-commits", + "--num", + "2", "--with-deps", "--recursive", "--run-cache", @@ -40,6 +42,7 @@ def test_fetch(mocker, dvc): all_branches=True, all_tags=True, all_commits=True, + num=2, with_deps=True, recursive=True, run_cache=True,