1
0
Fork 0
mirror of https://github.com/git/git.git synced 2024-05-29 17:56:09 +02:00
git/bundle-uri.c
Derrick Stolee 738e5245fa bundle-uri: parse bundle list in config format
When a bundle provider wants to operate independently from a Git remote,
they want to provide a single, consistent URI that users can use in
their 'git clone --bundle-uri' commands. At this point, the Git client
expects that URI to be a single bundle that can be unbundled and used to
bootstrap the rest of the clone from the Git server. This single bundle
cannot be re-used to assist with future incremental fetches.

To allow for the incremental fetch case, teach Git to understand a
bundle list that could be advertised at an independent bundle URI. Such
a bundle list is likely to be inspected by human readers, even if only
by the bundle provider creating the list. For this reason, we can take
our expected "key=value" pairs and instead format them using Git config
format.

Create bundle_uri_parse_config_format() to parse a file in config format
and convert that into a 'struct bundle_list' filled with its
understanding of the contents.

Be careful to use error_action CONFIG_ERROR_ERROR when calling
git_config_from_file_with_options() because the default action for
git_config_from_file() is to die() on a parsing error.  The current
warning isn't particularly helpful if it arises to a user, but it will
be made more verbose at a higher layer later.

Update 'test-tool bundle-uri' to take this config file format as input.
It uses a filename instead of stdin because there is no existing way to
parse a FILE pointer in the config machinery. Using
git_config_from_mem() is overly complicated and more likely to introduce
bugs than this simpler version.

Signed-off-by: Derrick Stolee <derrickstolee@github.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-10-12 09:13:24 -07:00

394 lines
8.6 KiB
C

#include "cache.h"
#include "bundle-uri.h"
#include "bundle.h"
#include "object-store.h"
#include "refs.h"
#include "run-command.h"
#include "hashmap.h"
#include "pkt-line.h"
#include "config.h"
static int compare_bundles(const void *hashmap_cmp_fn_data,
const struct hashmap_entry *he1,
const struct hashmap_entry *he2,
const void *id)
{
const struct remote_bundle_info *e1 =
container_of(he1, const struct remote_bundle_info, ent);
const struct remote_bundle_info *e2 =
container_of(he2, const struct remote_bundle_info, ent);
return strcmp(e1->id, id ? (const char *)id : e2->id);
}
void init_bundle_list(struct bundle_list *list)
{
memset(list, 0, sizeof(*list));
/* Implied defaults. */
list->mode = BUNDLE_MODE_ALL;
list->version = 1;
hashmap_init(&list->bundles, compare_bundles, NULL, 0);
}
static int clear_remote_bundle_info(struct remote_bundle_info *bundle,
void *data)
{
FREE_AND_NULL(bundle->id);
FREE_AND_NULL(bundle->uri);
return 0;
}
void clear_bundle_list(struct bundle_list *list)
{
if (!list)
return;
for_all_bundles_in_list(list, clear_remote_bundle_info, NULL);
hashmap_clear_and_free(&list->bundles, struct remote_bundle_info, ent);
}
int for_all_bundles_in_list(struct bundle_list *list,
bundle_iterator iter,
void *data)
{
struct remote_bundle_info *info;
struct hashmap_iter i;
hashmap_for_each_entry(&list->bundles, &i, info, ent) {
int result = iter(info, data);
if (result)
return result;
}
return 0;
}
static int summarize_bundle(struct remote_bundle_info *info, void *data)
{
FILE *fp = data;
fprintf(fp, "[bundle \"%s\"]\n", info->id);
fprintf(fp, "\turi = %s\n", info->uri);
return 0;
}
void print_bundle_list(FILE *fp, struct bundle_list *list)
{
const char *mode;
switch (list->mode) {
case BUNDLE_MODE_ALL:
mode = "all";
break;
case BUNDLE_MODE_ANY:
mode = "any";
break;
case BUNDLE_MODE_NONE:
default:
mode = "<unknown>";
}
fprintf(fp, "[bundle]\n");
fprintf(fp, "\tversion = %d\n", list->version);
fprintf(fp, "\tmode = %s\n", mode);
for_all_bundles_in_list(list, summarize_bundle, fp);
}
/**
* Given a key-value pair, update the state of the given bundle list.
* Returns 0 if the key-value pair is understood. Returns -1 if the key
* is not understood or the value is malformed.
*/
static int bundle_list_update(const char *key, const char *value,
struct bundle_list *list)
{
struct strbuf id = STRBUF_INIT;
struct remote_bundle_info lookup = REMOTE_BUNDLE_INFO_INIT;
struct remote_bundle_info *bundle;
const char *subsection, *subkey;
size_t subsection_len;
if (parse_config_key(key, "bundle", &subsection, &subsection_len, &subkey))
return -1;
if (!subsection_len) {
if (!strcmp(subkey, "version")) {
int version;
if (!git_parse_int(value, &version))
return -1;
if (version != 1)
return -1;
list->version = version;
return 0;
}
if (!strcmp(subkey, "mode")) {
if (!strcmp(value, "all"))
list->mode = BUNDLE_MODE_ALL;
else if (!strcmp(value, "any"))
list->mode = BUNDLE_MODE_ANY;
else
return -1;
return 0;
}
/* Ignore other unknown global keys. */
return 0;
}
strbuf_add(&id, subsection, subsection_len);
/*
* Check for an existing bundle with this <id>, or create one
* if necessary.
*/
lookup.id = id.buf;
hashmap_entry_init(&lookup.ent, strhash(lookup.id));
if (!(bundle = hashmap_get_entry(&list->bundles, &lookup, ent, NULL))) {
CALLOC_ARRAY(bundle, 1);
bundle->id = strbuf_detach(&id, NULL);
hashmap_entry_init(&bundle->ent, strhash(bundle->id));
hashmap_add(&list->bundles, &bundle->ent);
}
strbuf_release(&id);
if (!strcmp(subkey, "uri")) {
if (bundle->uri)
return -1;
bundle->uri = xstrdup(value);
return 0;
}
/*
* At this point, we ignore any information that we don't
* understand, assuming it to be hints for a heuristic the client
* does not currently understand.
*/
return 0;
}
static int config_to_bundle_list(const char *key, const char *value, void *data)
{
struct bundle_list *list = data;
return bundle_list_update(key, value, list);
}
int bundle_uri_parse_config_format(const char *uri,
const char *filename,
struct bundle_list *list)
{
int result;
struct config_options opts = {
.error_action = CONFIG_ERROR_ERROR,
};
result = git_config_from_file_with_options(config_to_bundle_list,
filename, list,
&opts);
if (!result && list->mode == BUNDLE_MODE_NONE) {
warning(_("bundle list at '%s' has no mode"), uri);
result = 1;
}
return result;
}
static char *find_temp_filename(void)
{
int fd;
struct strbuf name = STRBUF_INIT;
/*
* Find a temporary filename that is available. This is briefly
* racy, but unlikely to collide.
*/
fd = odb_mkstemp(&name, "bundles/tmp_uri_XXXXXX");
if (fd < 0) {
warning(_("failed to create temporary file"));
return NULL;
}
close(fd);
unlink(name.buf);
return strbuf_detach(&name, NULL);
}
static int download_https_uri_to_file(const char *file, const char *uri)
{
int result = 0;
struct child_process cp = CHILD_PROCESS_INIT;
FILE *child_in = NULL, *child_out = NULL;
struct strbuf line = STRBUF_INIT;
int found_get = 0;
strvec_pushl(&cp.args, "git-remote-https", uri, NULL);
cp.in = -1;
cp.out = -1;
if (start_command(&cp))
return 1;
child_in = fdopen(cp.in, "w");
if (!child_in) {
result = 1;
goto cleanup;
}
child_out = fdopen(cp.out, "r");
if (!child_out) {
result = 1;
goto cleanup;
}
fprintf(child_in, "capabilities\n");
fflush(child_in);
while (!strbuf_getline(&line, child_out)) {
if (!line.len)
break;
if (!strcmp(line.buf, "get"))
found_get = 1;
}
strbuf_release(&line);
if (!found_get) {
result = error(_("insufficient capabilities"));
goto cleanup;
}
fprintf(child_in, "get %s %s\n\n", uri, file);
cleanup:
if (child_in)
fclose(child_in);
if (finish_command(&cp))
return 1;
if (child_out)
fclose(child_out);
return result;
}
static int copy_uri_to_file(const char *filename, const char *uri)
{
const char *out;
if (starts_with(uri, "https:") ||
starts_with(uri, "http:"))
return download_https_uri_to_file(filename, uri);
if (skip_prefix(uri, "file://", &out))
uri = out;
/* Copy as a file */
return copy_file(filename, uri, 0);
}
static int unbundle_from_file(struct repository *r, const char *file)
{
int result = 0;
int bundle_fd;
struct bundle_header header = BUNDLE_HEADER_INIT;
struct string_list_item *refname;
struct strbuf bundle_ref = STRBUF_INIT;
size_t bundle_prefix_len;
if ((bundle_fd = read_bundle_header(file, &header)) < 0)
return 1;
if ((result = unbundle(r, &header, bundle_fd, NULL)))
return 1;
/*
* Convert all refs/heads/ from the bundle into refs/bundles/
* in the local repository.
*/
strbuf_addstr(&bundle_ref, "refs/bundles/");
bundle_prefix_len = bundle_ref.len;
for_each_string_list_item(refname, &header.references) {
struct object_id *oid = refname->util;
struct object_id old_oid;
const char *branch_name;
int has_old;
if (!skip_prefix(refname->string, "refs/heads/", &branch_name))
continue;
strbuf_setlen(&bundle_ref, bundle_prefix_len);
strbuf_addstr(&bundle_ref, branch_name);
has_old = !read_ref(bundle_ref.buf, &old_oid);
update_ref("fetched bundle", bundle_ref.buf, oid,
has_old ? &old_oid : NULL,
REF_SKIP_OID_VERIFICATION,
UPDATE_REFS_MSG_ON_ERR);
}
bundle_header_release(&header);
return result;
}
int fetch_bundle_uri(struct repository *r, const char *uri)
{
int result = 0;
char *filename;
if (!(filename = find_temp_filename())) {
result = -1;
goto cleanup;
}
if ((result = copy_uri_to_file(filename, uri))) {
warning(_("failed to download bundle from URI '%s'"), uri);
goto cleanup;
}
if ((result = !is_bundle(filename, 0))) {
warning(_("file at URI '%s' is not a bundle"), uri);
goto cleanup;
}
if ((result = unbundle_from_file(r, filename))) {
warning(_("failed to unbundle bundle from URI '%s'"), uri);
goto cleanup;
}
cleanup:
if (filename)
unlink(filename);
free(filename);
return result;
}
/**
* General API for {transport,connect}.c etc.
*/
int bundle_uri_parse_line(struct bundle_list *list, const char *line)
{
int result;
const char *equals;
struct strbuf key = STRBUF_INIT;
if (!strlen(line))
return error(_("bundle-uri: got an empty line"));
equals = strchr(line, '=');
if (!equals)
return error(_("bundle-uri: line is not of the form 'key=value'"));
if (line == equals || !*(equals + 1))
return error(_("bundle-uri: line has empty key or value"));
strbuf_add(&key, line, equals - line);
result = bundle_list_update(key.buf, equals + 1, list);
strbuf_release(&key);
return result;
}