@@ -35,9 +35,10 @@ void init_bundle_list(struct bundle_list *list)
static int clear_remote_bundle_info(struct remote_bundle_info *bundle,
void *data)
{
- free(bundle->id);
- free(bundle->uri);
+ FREE_AND_NULL(bundle->id);
+ FREE_AND_NULL(bundle->uri);
strbuf_release(&bundle->file);
+ bundle->unbundled = 0;
return 0;
}
@@ -334,18 +335,102 @@ static int unbundle_from_file(struct repository *r, const char *file)
return result;
}
+struct bundle_list_context {
+ struct repository *r;
+ struct bundle_list *list;
+ enum bundle_list_mode mode;
+ int count;
+ int depth;
+};
+
+/*
+ * This early definition is necessary because we use indirect recursion:
+ *
+ * While iterating through a bundle list that was downloaded as part
+ * of fetch_bundle_uri_internal(), iterator methods eventually call it
+ * again, but with depth + 1.
+ */
+static int fetch_bundle_uri_internal(struct repository *r,
+ struct remote_bundle_info *bundle,
+ int depth,
+ struct bundle_list *list);
+
+static int download_bundle_to_file(struct remote_bundle_info *bundle, void *data)
+{
+ struct bundle_list_context *ctx = data;
+
+ if (ctx->mode == BUNDLE_MODE_ANY && ctx->count)
+ return 0;
+
+ ctx->count++;
+ return fetch_bundle_uri_internal(ctx->r, bundle, ctx->depth + 1, ctx->list);
+}
+
+static int download_bundle_list(struct repository *r,
+ struct bundle_list *local_list,
+ struct bundle_list *global_list,
+ int depth)
+{
+ struct bundle_list_context ctx = {
+ .r = r,
+ .list = global_list,
+ .depth = depth + 1,
+ .mode = local_list->mode,
+ };
+
+ return for_all_bundles_in_list(local_list, download_bundle_to_file, &ctx);
+}
+
+static int fetch_bundle_list_in_config_format(struct repository *r,
+ struct bundle_list *global_list,
+ struct remote_bundle_info *bundle,
+ int depth)
+{
+ int result;
+ struct bundle_list list_from_bundle;
+
+ init_bundle_list(&list_from_bundle);
+
+ if ((result = parse_bundle_list_in_config_format(bundle->uri,
+ bundle->file.buf,
+ &list_from_bundle)))
+ goto cleanup;
+
+ if (list_from_bundle.mode == BUNDLE_MODE_NONE) {
+ warning(_("unrecognized bundle mode from URI '%s'"),
+ bundle->uri);
+ result = -1;
+ goto cleanup;
+ }
+
+ if ((result = download_bundle_list(r, &list_from_bundle,
+ global_list, depth)))
+ goto cleanup;
+
+cleanup:
+ clear_bundle_list(&list_from_bundle);
+ return result;
+}
+
/**
* This limits the recursion on fetch_bundle_uri_internal() when following
* bundle lists.
*/
static int max_bundle_uri_depth = 4;
+/**
+ * Recursively download all bundles advertised at the given URI
+ * to files. If the file is a bundle, then add it to the given
+ * 'list'. Otherwise, expect a bundle list and recurse on the
+ * URIs in that list according to the list mode (ANY or ALL).
+ */
static int fetch_bundle_uri_internal(struct repository *r,
- const char *uri,
- int depth)
+ struct remote_bundle_info *bundle,
+ int depth,
+ struct bundle_list *list)
{
int result = 0;
- struct strbuf filename = STRBUF_INIT;
+ struct remote_bundle_info *bcopy;
if (depth >= max_bundle_uri_depth) {
warning(_("exceeded bundle URI recursion limit (%d)"),
@@ -353,33 +438,125 @@ static int fetch_bundle_uri_internal(struct repository *r,
return -1;
}
- if ((result = find_temp_filename(&filename)))
+ if (!bundle->file.len &&
+ (result = find_temp_filename(&bundle->file)))
goto cleanup;
- if ((result = copy_uri_to_file(filename.buf, uri))) {
- warning(_("failed to download bundle from URI '%s'"), uri);
+ if ((result = copy_uri_to_file(bundle->file.buf, bundle->uri))) {
+ warning(_("failed to download bundle from URI '%s'"), bundle->uri);
goto cleanup;
}
- if ((result = !is_bundle(filename.buf, 0))) {
- warning(_("file at URI '%s' is not a bundle"), uri);
+ if ((result = !is_bundle(bundle->file.buf, 1))) {
+ result = fetch_bundle_list_in_config_format(
+ r, list, bundle, depth);
+ if (result)
+ warning(_("file at URI '%s' is not a bundle or bundle list"),
+ bundle->uri);
goto cleanup;
}
- if ((result = unbundle_from_file(r, filename.buf))) {
- warning(_("failed to unbundle bundle from URI '%s'"), uri);
- goto cleanup;
- }
+ /* Copy the bundle and insert it into the global list. */
+ CALLOC_ARRAY(bcopy, 1);
+ bcopy->id = xstrdup(bundle->id);
+ strbuf_init(&bcopy->file, 0);
+ strbuf_add(&bcopy->file, bundle->file.buf, bundle->file.len);
+ hashmap_entry_init(&bcopy->ent, strhash(bcopy->id));
+ hashmap_add(&list->bundles, &bcopy->ent);
cleanup:
- unlink(filename.buf);
- strbuf_release(&filename);
+ if (result)
+ unlink(bundle->file.buf);
return result;
}
+struct attempt_unbundle_context {
+ struct repository *r;
+ int success_count;
+ int failure_count;
+};
+
+static int attempt_unbundle(struct remote_bundle_info *info, void *data)
+{
+ struct attempt_unbundle_context *ctx = data;
+
+ if (info->unbundled || !unbundle_from_file(ctx->r, info->file.buf)) {
+ ctx->success_count++;
+ info->unbundled = 1;
+ } else {
+ ctx->failure_count++;
+ }
+
+ return 0;
+}
+
+static int unbundle_all_bundles(struct repository *r,
+ struct bundle_list *list)
+{
+ int last_success_count = -1;
+ struct attempt_unbundle_context ctx = {
+ .r = r,
+ };
+
+ /*
+ * Iterate through all bundles looking for ones that can
+ * successfully unbundle. If any succeed, then perhaps another
+ * will succeed in the next attempt.
+ */
+ while (last_success_count < ctx.success_count) {
+ last_success_count = ctx.success_count;
+
+ ctx.success_count = 0;
+ ctx.failure_count = 0;
+ for_all_bundles_in_list(list, attempt_unbundle, &ctx);
+ }
+
+ if (ctx.success_count)
+ git_config_set_multivar_gently("log.excludedecoration",
+ "refs/bundle/",
+ "refs/bundle/",
+ CONFIG_FLAGS_FIXED_VALUE |
+ CONFIG_FLAGS_MULTI_REPLACE);
+
+ if (ctx.failure_count)
+ warning(_("failed to unbundle %d bundles"),
+ ctx.failure_count);
+
+ return 0;
+}
+
+static int unlink_bundle(struct remote_bundle_info *info, void *data)
+{
+ if (info->file.buf)
+ unlink_or_warn(info->file.buf);
+ return 0;
+}
+
int fetch_bundle_uri(struct repository *r, const char *uri)
{
- return fetch_bundle_uri_internal(r, uri, 0);
+ int result;
+ struct bundle_list list;
+ struct remote_bundle_info bundle = {
+ .uri = xstrdup(uri),
+ .id = xstrdup("<root>"),
+ .file = STRBUF_INIT,
+ };
+
+ init_bundle_list(&list);
+
+ /* If a bundle is added to this global list, then it is required. */
+ list.mode = BUNDLE_MODE_ALL;
+
+ if ((result = fetch_bundle_uri_internal(r, &bundle, 0, &list)))
+ goto cleanup;
+
+ result = unbundle_all_bundles(r, &list);
+
+cleanup:
+ for_all_bundles_in_list(&list, unlink_bundle, NULL);
+ clear_bundle_list(&list);
+ clear_remote_bundle_info(&bundle, NULL);
+ return result;
}
/**
@@ -35,6 +35,12 @@ struct remote_bundle_info {
* an empty string.
*/
struct strbuf file;
+
+ /**
+ * If the bundle has been unbundled successfully, then
+ * this boolean is true.
+ */
+ unsigned unbundled:1;
};
#define REMOTE_BUNDLE_INFO_INIT { \
@@ -41,6 +41,72 @@ test_expect_success 'clone with file:// bundle' '
test_cmp expect actual
'
+# To get interesting tests for bundle lists, we need to construct a
+# somewhat-interesting commit history.
+#
+# ---------------- bundle-4
+#
+# 4
+# / \
+# ----|---|------- bundle-3
+# | |
+# | 3
+# | |
+# ----|---|------- bundle-2
+# | |
+# 2 |
+# | |
+# ----|---|------- bundle-1
+# \ /
+# 1
+# |
+# (previous commits)
+test_expect_success 'construct incremental bundle list' '
+ (
+ cd clone-from &&
+ git checkout -b base &&
+ test_commit 1 &&
+ git checkout -b left &&
+ test_commit 2 &&
+ git checkout -b right base &&
+ test_commit 3 &&
+ git checkout -b merge left &&
+ git merge right -m "4" &&
+
+ git bundle create bundle-1.bundle base &&
+ git bundle create bundle-2.bundle base..left &&
+ git bundle create bundle-3.bundle base..right &&
+ git bundle create bundle-4.bundle merge --not left right
+ )
+'
+
+test_expect_success 'clone bundle list (file, no heuristic)' '
+ cat >bundle-list <<-EOF &&
+ [bundle]
+ version = 1
+ mode = all
+
+ [bundle "bundle-1"]
+ uri = file://$(pwd)/clone-from/bundle-1.bundle
+
+ [bundle "bundle-2"]
+ uri = file://$(pwd)/clone-from/bundle-2.bundle
+
+ [bundle "bundle-3"]
+ uri = file://$(pwd)/clone-from/bundle-3.bundle
+
+ [bundle "bundle-4"]
+ uri = file://$(pwd)/clone-from/bundle-4.bundle
+ EOF
+
+ git clone --bundle-uri="file://$(pwd)/bundle-list" . clone-list-file &&
+ for oid in $(git -C clone-from for-each-ref --format="%(objectname)")
+ do
+ git -C clone-list-file rev-parse $oid || return 1
+ done
+'
+
+
#########################################################################
# HTTP tests begin here
@@ -75,6 +141,33 @@ test_expect_success 'clone HTTP bundle' '
test_config -C clone-http log.excludedecoration refs/bundle/
'
+test_expect_success 'clone bundle list (HTTP, no heuristic)' '
+ cp clone-from/bundle-*.bundle "$HTTPD_DOCUMENT_ROOT_PATH/" &&
+ cat >"$HTTPD_DOCUMENT_ROOT_PATH/bundle-list" <<-EOF &&
+ [bundle]
+ version = 1
+ mode = all
+
+ [bundle "bundle-1"]
+ uri = $HTTPD_URL/bundle-1.bundle
+
+ [bundle "bundle-2"]
+ uri = $HTTPD_URL/bundle-2.bundle
+
+ [bundle "bundle-3"]
+ uri = $HTTPD_URL/bundle-3.bundle
+
+ [bundle "bundle-4"]
+ uri = $HTTPD_URL/bundle-4.bundle
+ EOF
+
+ git clone --bundle-uri="$HTTPD_URL/bundle-list" . clone-list-http &&
+ for oid in $(git -C clone-from for-each-ref --format="%(objectname)")
+ do
+ git -C clone-list-http rev-parse $oid || return 1
+ done
+'
+
# Do not add tests here unless they use the HTTP server, as they will
# not run unless the HTTP dependencies exist.