bundle: create filtered bundles

A previous change allowed Git to parse bundles with the 'filter' capability. Now, teach Git to create bundles with this option. Some rearranging of code is required to get the option parsing in the correct spot. There are now two reasons why we might need capabilities (a new hash algorithm or an object filter) so that is pulled out into a place where we can check both at the same time. The --filter option is parsed as part of setup_revisions(), but it expected the --objects flag, too. That flag is somewhat implied by 'git bundle' because it creates a pack-file walking objects, but there is also a walk that walks the revision range expecting only commits. Make this parsing work by setting 'revs.tree_objects' and 'revs.blob_objects' before the call to setup_revisions(). Signed-off-by: Derrick Stolee <derrickstolee@github.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed Mar 9, 2022 at 16:01 UTC f18b512bbba8ee508fec64698f581920bfa5b46d
2 files changed +90 -11
bundle.c
+42 -11
@@ -332,6 +332,9 @@ static int write_pack_data(int bundle_fd, struct rev_info *revs, struct strvec *
332 "--stdout", "--thin", "--delta-base-offset",
333 NULL);
334 strvec_pushv(&pack_objects.args, pack_options->v);
335 + if (revs->filter.choice)
336 + strvec_pushf(&pack_objects.args, "--filter=%s",
337 + list_objects_filter_spec(&revs->filter));
338 pack_objects.in = -1;
339 pack_objects.out = bundle_fd;
340 pack_objects.git_cmd = 1;
@@ -499,10 +502,37 @@ int create_bundle(struct repository *r, const char *path,
502 int bundle_to_stdout;
503 int ref_count = 0;
504 struct rev_info revs, revs_copy;
502 - int min_version = the_hash_algo == &hash_algos[GIT_HASH_SHA1] ? 2 : 3;
505 + int min_version = 2;
506 struct bundle_prerequisites_info bpi;
507 int i;
508
509 + /* init revs to list objects for pack-objects later */
510 + save_commit_buffer = 0;
511 + repo_init_revisions(r, &revs, NULL);
512 +
513 + /*
514 + * Pre-initialize the '--objects' flag so we can parse a
515 + * --filter option successfully.
516 + */
517 + revs.tree_objects = revs.blob_objects = 1;
518 +
519 + argc = setup_revisions(argc, argv, &revs, NULL);
520 +
521 + /*
522 + * Reasons to require version 3:
523 + *
524 + * 1. @object-format is required because our hash algorithm is not
525 + * SHA1.
526 + * 2. @filter is required because we parsed an object filter.
527 + */
528 + if (the_hash_algo != &hash_algos[GIT_HASH_SHA1] || revs.filter.choice)
529 + min_version = 3;
530 +
531 + if (argc > 1) {
532 + error(_("unrecognized argument: %s"), argv[1]);
533 + goto err;
534 + }
535 +
536 bundle_to_stdout = !strcmp(path, "-");
537 if (bundle_to_stdout)
538 bundle_fd = 1;
@@ -525,17 +555,14 @@ int create_bundle(struct repository *r, const char *path,
555 write_or_die(bundle_fd, capability, strlen(capability));
556 write_or_die(bundle_fd, the_hash_algo->name, strlen(the_hash_algo->name));
557 write_or_die(bundle_fd, "\n", 1);
528 - }
529 -
530 - /* init revs to list objects for pack-objects later */
531 - save_commit_buffer = 0;
532 - repo_init_revisions(r, &revs, NULL);
558
534 - argc = setup_revisions(argc, argv, &revs, NULL);
535 -
536 - if (argc > 1) {
537 - error(_("unrecognized argument: %s"), argv[1]);
538 - goto err;
559 + if (revs.filter.choice) {
560 + const char *value = expand_list_objects_filter_spec(&revs.filter);
561 + capability = "@filter=";
562 + write_or_die(bundle_fd, capability, strlen(capability));
563 + write_or_die(bundle_fd, value, strlen(value));
564 + write_or_die(bundle_fd, "\n", 1);
565 + }
566 }
567
568 /* save revs.pending in revs_copy for later use */
@@ -558,6 +585,10 @@ int create_bundle(struct repository *r, const char *path,
585 bpi.fd = bundle_fd;
586 bpi.pending = &revs_copy.pending;
587
588 + /*
589 + * Remove any object walking here. We only care about commits and
590 + * tags here. The revs_copy has the right instances of these values.
591 + */
592 revs.blob_objects = revs.tree_objects = 0;
593 traverse_commit_list(&revs, write_bundle_prerequisites, NULL, &bpi);
594 object_array_remove_duplicates(&revs_copy.pending);
t/t6020-bundle-misc.sh
+48
@@ -487,4 +487,52 @@ test_expect_success 'unfiltered bundle with --objects' '
487 test_cmp expect actual
488 '
489
490 +for filter in "blob:none" "tree:0" "tree:1" "blob:limit=100"
491 +do
492 + test_expect_success "filtered bundle: $filter" '
493 + test_when_finished rm -rf .git/objects/pack cloned unbundled &&
494 + git bundle create partial.bdl \
495 + --all \
496 + --filter=$filter &&
497 +
498 + git bundle verify partial.bdl >unfiltered &&
499 + make_user_friendly_and_stable_output <unfiltered >actual &&
500 +
501 + cat >expect <<-EOF &&
502 + The bundle contains these 10 refs:
503 + <COMMIT-P> refs/heads/main
504 + <COMMIT-N> refs/heads/release
505 + <COMMIT-D> refs/heads/topic/1
506 + <COMMIT-H> refs/heads/topic/2
507 + <COMMIT-D> refs/pull/1/head
508 + <COMMIT-G> refs/pull/2/head
509 + <TAG-1> refs/tags/v1
510 + <TAG-2> refs/tags/v2
511 + <TAG-3> refs/tags/v3
512 + <COMMIT-P> HEAD
513 + The bundle uses this filter: $filter
514 + The bundle records a complete history.
515 + EOF
516 + test_cmp expect actual &&
517 +
518 + test_config uploadpack.allowfilter 1 &&
519 + test_config uploadpack.allowanysha1inwant 1 &&
520 + git clone --no-local --filter=$filter --bare "file://$(pwd)" cloned &&
521 +
522 + git init unbundled &&
523 + git -C unbundled bundle unbundle ../partial.bdl >ref-list.txt &&
524 +
525 + # Count the same number of reachable objects.
526 + reflist=$(git for-each-ref --format="%(objectname)") &&
527 + git rev-list --objects --filter=$filter --missing=allow-any \
528 + $reflist >expect &&
529 + for repo in cloned unbundled
530 + do
531 + git -C $repo rev-list --objects --missing=allow-any \
532 + $reflist >actual &&
533 + test_cmp expect actual || return 1
534 + done
535 + '
536 +done
537 +
538 test_done