Example #1
0
gboolean
ostree_builtin_checkout (int argc, char **argv, GCancellable *cancellable, GError **error)
{
  GOptionContext *context;
  glnx_unref_object OstreeRepo *repo = NULL;
  gboolean ret = FALSE;
  const char *commit;
  const char *destination;
  g_autofree char *resolved_commit = NULL;

  context = g_option_context_new ("COMMIT [DESTINATION] - Check out a commit into a filesystem tree");

  if (!ostree_option_context_parse (context, options, &argc, &argv, OSTREE_BUILTIN_FLAG_NONE, &repo, cancellable, error))
    goto out;

  if (opt_disable_fsync)
    ostree_repo_set_disable_fsync (repo, TRUE);

  if (argc < 2)
    {
      gchar *help = g_option_context_get_help (context, TRUE, NULL);
      g_printerr ("%s\n", help);
      g_free (help);
      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "COMMIT must be specified");
      goto out;
    }

  if (opt_from_stdin || opt_from_file)
    {
      destination = argv[1];

      if (!process_many_checkouts (repo, destination, cancellable, error))
        goto out;
    }
  else
    {
      commit = argv[1];
      if (argc < 3)
        destination = commit;
      else
        destination = argv[2];

      if (!ostree_repo_resolve_rev (repo, commit, FALSE, &resolved_commit, error))
        goto out;

      if (!process_one_checkout (repo, resolved_commit, opt_subpath,
                                 destination,
                                 cancellable, error))
        goto out;
    }

  ret = TRUE;
 out:
  if (context)
    g_option_context_free (context);
  return ret;
}
Example #2
0
gboolean
ostree_builtin_commit (int argc, char **argv, GCancellable *cancellable, GError **error)
{
  GOptionContext *context;
  glnx_unref_object OstreeRepo *repo = NULL;
  gboolean ret = FALSE;
  gboolean skip_commit = FALSE;
  g_autoptr(GFile) object_to_commit = NULL;
  g_autofree char *parent = NULL;
  g_autofree char *commit_checksum = NULL;
  g_autoptr(GFile) root = NULL;
  g_autoptr(GVariant) metadata = NULL;
  g_autoptr(GVariant) detached_metadata = NULL;
  glnx_unref_object OstreeMutableTree *mtree = NULL;
  g_autofree char *tree_type = NULL;
  g_autoptr(GHashTable) mode_adds = NULL;
  OstreeRepoCommitModifierFlags flags = 0;
  OstreeRepoCommitModifier *modifier = NULL;
  OstreeRepoTransactionStats stats;

  context = g_option_context_new ("[PATH] - Commit a new revision");

  if (!ostree_option_context_parse (context, options, &argc, &argv, OSTREE_BUILTIN_FLAG_NONE, &repo, cancellable, error))
    goto out;

  if (!ostree_ensure_repo_writable (repo, error))
    goto out;

  if (opt_statoverride_file)
    {
      if (!parse_statoverride_file (&mode_adds, cancellable, error))
        goto out;
    }

  if (opt_metadata_strings)
    {
      if (!parse_keyvalue_strings (opt_metadata_strings,
                                   &metadata, error))
        goto out;
    }
  if (opt_detached_metadata_strings)
    {
      if (!parse_keyvalue_strings (opt_detached_metadata_strings,
                                   &detached_metadata, error))
        goto out;
    }
      
  if (!opt_branch)
    {
      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "A branch must be specified with --branch");
      goto out;
    }

  if (opt_no_xattrs)
    flags |= OSTREE_REPO_COMMIT_MODIFIER_FLAGS_SKIP_XATTRS;
  if (opt_generate_sizes)
    flags |= OSTREE_REPO_COMMIT_MODIFIER_FLAGS_GENERATE_SIZES;
  if (opt_disable_fsync)
    ostree_repo_set_disable_fsync (repo, TRUE);

  if (flags != 0
      || opt_owner_uid >= 0
      || opt_owner_gid >= 0
      || opt_statoverride_file != NULL
      || opt_no_xattrs)
    {
      modifier = ostree_repo_commit_modifier_new (flags, commit_filter, mode_adds, NULL);
    }

  if (!ostree_repo_resolve_rev (repo, opt_branch, TRUE, &parent, error))
    goto out;

  if (!opt_subject && !opt_body)
    {
      if (!commit_editor (repo, opt_branch, &opt_subject, &opt_body, cancellable, error))
        goto out;
    }

  if (!opt_subject)
    {
      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "A subject must be specified with --subject");
      goto out;
    }

  if (!ostree_repo_prepare_transaction (repo, NULL, cancellable, error))
    goto out;

  if (opt_link_checkout_speedup && !ostree_repo_scan_hardlinks (repo, cancellable, error))
    goto out;

  mtree = ostree_mutable_tree_new ();

  if (argc <= 1 && (opt_trees == NULL || opt_trees[0] == NULL))
    {
      char *current_dir = g_get_current_dir ();
      object_to_commit = g_file_new_for_path (current_dir);
      g_free (current_dir);

      if (!ostree_repo_write_directory_to_mtree (repo, object_to_commit, mtree, modifier,
                                                 cancellable, error))
        goto out;
    }
  else if (opt_trees != NULL)
    {
      const char *const*tree_iter;
      const char *tree;
      const char *eq;

      for (tree_iter = (const char *const*)opt_trees; *tree_iter; tree_iter++)
        {
          tree = *tree_iter;

          eq = strchr (tree, '=');
          if (!eq)
            {
              g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "Missing type in tree specification '%s'", tree);
              goto out;
            }
          g_free (tree_type);
          tree_type = g_strndup (tree, eq - tree);
          tree = eq + 1;

          g_clear_object (&object_to_commit);
          if (strcmp (tree_type, "dir") == 0)
            {
              object_to_commit = g_file_new_for_path (tree);
              if (!ostree_repo_write_directory_to_mtree (repo, object_to_commit, mtree, modifier,
                                                         cancellable, error))
                goto out;
            }
          else if (strcmp (tree_type, "tar") == 0)
            {
              object_to_commit = g_file_new_for_path (tree);
              if (!ostree_repo_write_archive_to_mtree (repo, object_to_commit, mtree, modifier,
                                                       opt_tar_autocreate_parents,
                                                       cancellable, error))
                goto out;
            }
          else if (strcmp (tree_type, "ref") == 0)
            {
              if (!ostree_repo_read_commit (repo, tree, &object_to_commit, NULL, cancellable, error))
                goto out;

              if (!ostree_repo_write_directory_to_mtree (repo, object_to_commit, mtree, modifier,
                                                         cancellable, error))
                goto out;
            }
          else
            {
              g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "Invalid tree type specification '%s'", tree_type);
              goto out;
            }
        }
    }
  else
    {
      g_assert (argc > 1);
      object_to_commit = g_file_new_for_path (argv[1]);
      if (!ostree_repo_write_directory_to_mtree (repo, object_to_commit, mtree, modifier,
                                                 cancellable, error))
        goto out;
    }

  if (mode_adds && g_hash_table_size (mode_adds) > 0)
    {
      GHashTableIter hash_iter;
      gpointer key, value;

      g_hash_table_iter_init (&hash_iter, mode_adds);

      while (g_hash_table_iter_next (&hash_iter, &key, &value))
        {
          g_printerr ("Unmatched statoverride path: %s\n", (char*)key);
        }
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Unmatched statoverride paths");
      goto out;
    }

  if (!ostree_repo_write_mtree (repo, mtree, &root, cancellable, error))
    goto out;

  if (opt_skip_if_unchanged && parent)
    {
      g_autoptr(GFile) parent_root;

      if (!ostree_repo_read_commit (repo, parent, &parent_root, NULL, cancellable, error))
        goto out;

      if (g_file_equal (root, parent_root))
        skip_commit = TRUE;
    }

  if (!skip_commit)
    {
      gboolean update_summary;
      if (!ostree_repo_write_commit (repo, parent, opt_subject, opt_body, metadata,
                                     OSTREE_REPO_FILE (root),
                                     &commit_checksum, cancellable, error))
        goto out;

      if (detached_metadata)
        {
          if (!ostree_repo_write_commit_detached_metadata (repo, commit_checksum,
                                                           detached_metadata,
                                                           cancellable, error))
            goto out;
        }

      if (opt_key_ids)
        {
          char **iter;

          for (iter = opt_key_ids; iter && *iter; iter++)
            {
              const char *keyid = *iter;

              if (!ostree_repo_sign_commit (repo,
                                            commit_checksum,
                                            keyid,
                                            opt_gpg_homedir,
                                            cancellable,
                                            error))
                goto out;
            }
        }

      ostree_repo_transaction_set_ref (repo, NULL, opt_branch, commit_checksum);

      if (!ostree_repo_commit_transaction (repo, &stats, cancellable, error))
        goto out;

      /* The default for this option is FALSE, even for archive-z2 repos,
       * because ostree supports multiple processes committing to the same
       * repo (but different refs) concurrently, and in fact gnome-continuous
       * actually does this.  In that context it's best to update the summary
       * explicitly instead of automatically here. */
      if (!ot_keyfile_get_boolean_with_default (ostree_repo_get_config (repo), "core",
                                                "commit-update-summary", FALSE,
                                                &update_summary, error))
        goto out;

      if (update_summary && !ostree_repo_regenerate_summary (repo,
                                                             NULL,
                                                             cancellable,
                                                             error))
        goto out;
    }
  else
    {
      commit_checksum = g_strdup (parent);
    }

  if (opt_table_output)
    {
      g_print ("Commit: %s\n", commit_checksum);
      g_print ("Metadata Total: %u\n", stats.metadata_objects_total);
      g_print ("Metadata Written: %u\n", stats.metadata_objects_written);
      g_print ("Content Total: %u\n", stats.content_objects_total);
      g_print ("Content Written: %u\n", stats.content_objects_written);
      g_print ("Content Bytes Written: %" G_GUINT64_FORMAT "\n", stats.content_bytes_written);
    }
  else
    {
      g_print ("%s\n", commit_checksum);
    }

  ret = TRUE;
 out:
  if (repo)
    ostree_repo_abort_transaction (repo, cancellable, NULL);
  if (context)
    g_option_context_free (context);
  if (modifier)
    ostree_repo_commit_modifier_unref (modifier);
  return ret;
}
gboolean
rpmostree_db_option_context_parse (GOptionContext *context,
                                   const GOptionEntry *main_entries,
                                   int *argc, char ***argv,
                                   OstreeRepo **out_repo,
                                   GFile **out_rpmdbdir,
                                   gboolean *out_rpmdbdir_is_tmp,
                                   GCancellable *cancellable, GError **error)
{
  gs_unref_object OstreeRepo *repo = NULL;
  gs_unref_object GFile *rpmdbdir = NULL;
  gboolean rpmdbdir_is_tmp = FALSE;
  gboolean success = FALSE;

  /* Entries are listed in --help output in the order added.  We add the
   * main entries ourselves so that we can add the --repo entry first. */

  g_option_context_add_main_entries (context, global_entries, NULL);

  if (!rpmostree_option_context_parse (context, main_entries, argc, argv, error))
    goto out;

  if (opt_repo == NULL)
    {
      gs_unref_object OstreeSysroot *sysroot = NULL;

      sysroot = ostree_sysroot_new_default ();
      if (!ostree_sysroot_load (sysroot, cancellable, error))
        goto out;

      if (!ostree_sysroot_get_repo (sysroot, &repo, cancellable, error))
        goto out;
    }
  else
    {
      gs_unref_object GFile *repo_file = g_file_new_for_path (opt_repo);

      repo = ostree_repo_new (repo_file);
      if (!ostree_repo_open (repo, cancellable, error))
        goto out;
    }

  if (rpmReadConfigFiles (NULL, NULL))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "rpm failed to init: %s", rpmlogMessage ());
      goto out;
    }

  if (opt_rpmdbdir != NULL)
    {
      rpmdbdir = g_file_new_for_path (opt_rpmdbdir);
    }
  else
    {
      /* tmp on tmpfs is much faster than /var/tmp,
       * and the rpmdb itself shouldn't be too big. */
      gs_free char *tmpd = g_mkdtemp (g_strdup ("/tmp/rpm-ostree.XXXXXX"));
      rpmdbdir = g_file_new_for_path (tmpd);
      rpmdbdir_is_tmp = TRUE;
      ostree_repo_set_disable_fsync (repo, TRUE);
    }

  gs_transfer_out_value (out_repo, &repo);
  gs_transfer_out_value (out_rpmdbdir, &rpmdbdir);

  if (out_rpmdbdir_is_tmp != NULL)
    *out_rpmdbdir_is_tmp = rpmdbdir_is_tmp;

  success = TRUE;

out:
  return success;
}
Example #4
0
gboolean
ostree_builtin_commit (int argc, char **argv, GCancellable *cancellable, GError **error)
{
  GOptionContext *context;
  gs_unref_object OstreeRepo *repo = NULL;
  gboolean ret = FALSE;
  gboolean skip_commit = FALSE;
  gs_unref_object GFile *arg = NULL;
  gs_free char *parent = NULL;
  gs_free char *commit_checksum = NULL;
  gs_unref_object GFile *root = NULL;
  gs_unref_variant GVariant *metadata = NULL;
  gs_unref_variant GVariant *detached_metadata = NULL;
  gs_unref_object OstreeMutableTree *mtree = NULL;
  gs_free char *tree_type = NULL;
  gs_unref_hashtable GHashTable *mode_adds = NULL;
  OstreeRepoCommitModifierFlags flags = 0;
  OstreeRepoCommitModifier *modifier = NULL;
  OstreeRepoTransactionStats stats;

  context = g_option_context_new ("[PATH] - Commit a new revision");

  if (!ostree_option_context_parse (context, options, &argc, &argv, OSTREE_BUILTIN_FLAG_NONE, &repo, cancellable, error))
    goto out;

  if (opt_statoverride_file)
    {
      if (!parse_statoverride_file (&mode_adds, cancellable, error))
        goto out;
    }

  if (opt_metadata_strings)
    {
      if (!parse_keyvalue_strings (opt_metadata_strings,
                                   &metadata, error))
        goto out;
    }
  if (opt_detached_metadata_strings)
    {
      if (!parse_keyvalue_strings (opt_detached_metadata_strings,
                                   &detached_metadata, error))
        goto out;
    }
      
  if (!opt_branch)
    {
      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "A branch must be specified with --branch");
      goto out;
    }

  if (opt_no_xattrs)
    flags |= OSTREE_REPO_COMMIT_MODIFIER_FLAGS_SKIP_XATTRS;
  if (opt_generate_sizes)
    flags |= OSTREE_REPO_COMMIT_MODIFIER_FLAGS_GENERATE_SIZES;
  if (opt_disable_fsync)
    ostree_repo_set_disable_fsync (repo, TRUE);

  if (flags != 0
      || opt_owner_uid >= 0
      || opt_owner_gid >= 0
      || opt_statoverride_file != NULL
      || opt_no_xattrs)
    {
      modifier = ostree_repo_commit_modifier_new (flags, commit_filter, mode_adds, NULL);
    }

  if (!ostree_repo_resolve_rev (repo, opt_branch, TRUE, &parent, error))
    goto out;

  if (!opt_subject && !opt_body)
    {
      if (!commit_editor (repo, opt_branch, &opt_subject, &opt_body, cancellable, error))
        goto out;
    }

  if (!opt_subject)
    {
      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "A subject must be specified with --subject");
      goto out;
    }

  if (!ostree_repo_prepare_transaction (repo, NULL, cancellable, error))
    goto out;

  if (opt_link_checkout_speedup && !ostree_repo_scan_hardlinks (repo, cancellable, error))
    goto out;

  mtree = ostree_mutable_tree_new ();

  if (argc <= 1 && (opt_trees == NULL || opt_trees[0] == NULL))
    {
      char *current_dir = g_get_current_dir ();
      arg = g_file_new_for_path (current_dir);
      g_free (current_dir);

      if (!ostree_repo_write_directory_to_mtree (repo, arg, mtree, modifier,
                                                 cancellable, error))
        goto out;
    }
  else if (opt_trees != NULL)
    {
      const char *const*tree_iter;
      const char *tree;
      const char *eq;

      for (tree_iter = (const char *const*)opt_trees; *tree_iter; tree_iter++)
        {
          tree = *tree_iter;

          eq = strchr (tree, '=');
          if (!eq)
            {
              g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "Missing type in tree specification '%s'", tree);
              goto out;
            }
          g_free (tree_type);
          tree_type = g_strndup (tree, eq - tree);
          tree = eq + 1;

          g_clear_object (&arg);
          if (strcmp (tree_type, "dir") == 0)
            {
              arg = g_file_new_for_path (tree);
              if (!ostree_repo_write_directory_to_mtree (repo, arg, mtree, modifier,
                                                         cancellable, error))
                goto out;
            }
          else if (strcmp (tree_type, "tar") == 0)
            {
              arg = g_file_new_for_path (tree);
              if (!ostree_repo_write_archive_to_mtree (repo, arg, mtree, modifier,
                                                       opt_tar_autocreate_parents,
                                                       cancellable, error))
                goto out;
            }
          else if (strcmp (tree_type, "ref") == 0)
            {
              if (!ostree_repo_read_commit (repo, tree, &arg, NULL, cancellable, error))
                goto out;

              if (!ostree_repo_write_directory_to_mtree (repo, arg, mtree, modifier,
                                                         cancellable, error))
                goto out;
            }
          else
            {
              g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "Invalid tree type specification '%s'", tree_type);
              goto out;
            }
        }
    }
  else
    {
      g_assert (argc > 1);
      arg = g_file_new_for_path (argv[1]);
      if (!ostree_repo_write_directory_to_mtree (repo, arg, mtree, modifier,
                                                 cancellable, error))
        goto out;
    }

  if (mode_adds && g_hash_table_size (mode_adds) > 0)
    {
      GHashTableIter hash_iter;
      gpointer key, value;

      g_hash_table_iter_init (&hash_iter, mode_adds);

      while (g_hash_table_iter_next (&hash_iter, &key, &value))
        {
          g_printerr ("Unmatched statoverride path: %s\n", (char*)key);
        }
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Unmatched statoverride paths");
      goto out;
    }

  if (!ostree_repo_write_mtree (repo, mtree, &root, cancellable, error))
    goto out;

  if (opt_skip_if_unchanged && parent)
    {
      gs_unref_object GFile *parent_root;

      if (!ostree_repo_read_commit (repo, parent, &parent_root, NULL, cancellable, error))
        goto out;

      if (g_file_equal (root, parent_root))
        skip_commit = TRUE;
    }

  if (!skip_commit)
    {
      if (!ostree_repo_write_commit (repo, parent, opt_subject, opt_body, metadata,
                                     OSTREE_REPO_FILE (root),
                                     &commit_checksum, cancellable, error))
        goto out;

      if (detached_metadata)
        {
          if (!ostree_repo_write_commit_detached_metadata (repo, commit_checksum,
                                                           detached_metadata,
                                                           cancellable, error))
            goto out;
        }

#ifdef HAVE_GPGME
      if (opt_key_ids)
        {
          char **iter;

          for (iter = opt_key_ids; iter && *iter; iter++)
            {
              const char *keyid = *iter;

              if (!ostree_repo_sign_commit (repo,
                                            commit_checksum,
                                            keyid,
                                            opt_gpg_homedir,
                                            cancellable,
                                            error))
                goto out;
            }
        }
#endif

      ostree_repo_transaction_set_ref (repo, NULL, opt_branch, commit_checksum);

      if (!ostree_repo_commit_transaction (repo, &stats, cancellable, error))
        goto out;
    }
  else
    {
      commit_checksum = g_strdup (parent);
    }

  if (opt_table_output)
    {
      g_print ("Commit: %s\n", commit_checksum);
      g_print ("Metadata Total: %u\n", stats.metadata_objects_total);
      g_print ("Metadata Written: %u\n", stats.metadata_objects_written);
      g_print ("Content Total: %u\n", stats.content_objects_total);
      g_print ("Content Written: %u\n", stats.content_objects_written);
      g_print ("Content Bytes Written: %" G_GUINT64_FORMAT "\n", stats.content_bytes_written);
    }
  else
    {
      g_print ("%s\n", commit_checksum);
    }

  ret = TRUE;
 out:
  if (repo)
    ostree_repo_abort_transaction (repo, cancellable, NULL);
  if (context)
    g_option_context_free (context);
  if (modifier)
    ostree_repo_commit_modifier_unref (modifier);
  return ret;
}
gboolean
flatpak_builtin_build_commit_from (int argc, char **argv, GCancellable *cancellable, GError **error)
{
  g_autoptr(GOptionContext) context = NULL;
  g_autoptr(GFile) dst_repofile = NULL;
  g_autoptr(OstreeRepo) dst_repo = NULL;
  g_autoptr(GFile) src_repofile = NULL;
  g_autoptr(OstreeRepo) src_repo = NULL;
  g_autofree char *src_repo_uri = NULL;
  const char *dst_repo_arg;
  const char **dst_refs;
  int n_dst_refs = 0;
  g_autoptr(FlatpakRepoTransaction) transaction = NULL;
  g_autoptr(GPtrArray) src_refs = NULL;
  g_autoptr(GPtrArray) resolved_src_refs = NULL;
  OstreeRepoCommitState src_commit_state;
  struct timespec ts;
  guint64 timestamp;
  int i;

  context = g_option_context_new (_("DST-REPO [DST-REF…] - Make a new commit from existing commits"));
  g_option_context_set_translation_domain (context, GETTEXT_PACKAGE);

  if (!flatpak_option_context_parse (context, options, &argc, &argv, FLATPAK_BUILTIN_FLAG_NO_DIR, NULL, cancellable, error))
    return FALSE;

  if (argc < 2)
    return usage_error (context, _("DST-REPO must be specified"), error);

  dst_repo_arg = argv[1];

  dst_refs = (const char **) argv + 2;
  n_dst_refs = argc - 2;

  if (opt_src_repo == NULL && n_dst_refs != 1)
    return usage_error (context, _("If --src-repo is not specified, exactly one destination ref must be specified"), error);

  if (opt_src_ref != NULL && n_dst_refs != 1)
    return usage_error (context, _("If --src-ref is specified, exactly one destination ref must be specified"), error);

  if (opt_src_repo == NULL && opt_src_ref == NULL)
    return flatpak_fail (error, _("Either --src-repo or --src-ref must be specified."));

  /* Always create a commit if we're eol:ing, even though the app is the same */
  if (opt_endoflife != NULL || opt_endoflife_rebase != NULL)
    opt_force = TRUE;

  if (opt_endoflife_rebase)
    {
      opt_endoflife_rebase_new = g_new0 (char *, g_strv_length (opt_endoflife_rebase));

      for (i = 0; opt_endoflife_rebase[i] != NULL; i++)
        {
          char *rebase_old = opt_endoflife_rebase[i];
          char *rebase_new = strchr (rebase_old, '=');

          if (rebase_new == NULL) {
            return usage_error (context, _("Invalid argument format of use  --end-of-life-rebase=OLDID=NEWID"), error);
          }
          *rebase_new = 0;
          rebase_new++;

          if (!flatpak_is_valid_name (rebase_old, error))
            return glnx_prefix_error (error, _("Invalid name %s in --end-of-life-rebase"), rebase_old);

          if (!flatpak_is_valid_name (rebase_new, error))
            return glnx_prefix_error (error, _("Invalid name %s in --end-of-life-rebase"), rebase_new);

          opt_endoflife_rebase_new[i] = rebase_new;
        }
    }

  if (opt_timestamp)
    {
      if (!parse_datetime (&ts, opt_timestamp, NULL))
        return flatpak_fail (error, _("Could not parse '%s'"), opt_timestamp);
    }

  dst_repofile = g_file_new_for_commandline_arg (dst_repo_arg);
  if (!g_file_query_exists (dst_repofile, cancellable))
    return flatpak_fail (error, _("'%s' is not a valid repository"), dst_repo_arg);

  dst_repo = ostree_repo_new (dst_repofile);
  if (!ostree_repo_open (dst_repo, cancellable, error))
    return FALSE;

  if (opt_disable_fsync)
    ostree_repo_set_disable_fsync (dst_repo, TRUE);

  if (opt_src_repo)
    {
      src_repofile = g_file_new_for_commandline_arg (opt_src_repo);
      if (!g_file_query_exists (src_repofile, cancellable))
        return flatpak_fail (error, _("'%s' is not a valid repository"), opt_src_repo);

      src_repo_uri = g_file_get_uri (src_repofile);
      src_repo = ostree_repo_new (src_repofile);
      if (!ostree_repo_open (src_repo, cancellable, error))
        return FALSE;
    }
  else
    {
      src_repo = g_object_ref (dst_repo);
    }

  src_refs = g_ptr_array_new_with_free_func (g_free);
  if (opt_src_ref)
    {
      g_assert (n_dst_refs == 1);
      g_ptr_array_add (src_refs, g_strdup (opt_src_ref));
    }
  else
    {
      g_assert (opt_src_repo != NULL);
      if (n_dst_refs == 0)
        {
          g_autofree const char **keys = NULL;
          g_autoptr(GHashTable) all_src_refs = NULL;

          if (!ostree_repo_list_refs (src_repo, NULL, &all_src_refs,
                                      cancellable, error))
            return FALSE;

          keys = (const char **) g_hash_table_get_keys_as_array (all_src_refs, NULL);

          for (i = 0; keys[i] != NULL; i++)
            {
              if (g_str_has_prefix (keys[i], "runtime/") ||
                  g_str_has_prefix (keys[i], "app/"))
                g_ptr_array_add (src_refs, g_strdup (keys[i]));
            }
          n_dst_refs = src_refs->len;
          dst_refs = (const char **) src_refs->pdata;
        }
      else
        {
          for (i = 0; i < n_dst_refs; i++)
            g_ptr_array_add (src_refs, g_strdup (dst_refs[i]));
        }
    }

  resolved_src_refs = g_ptr_array_new_with_free_func (g_free);
  for (i = 0; i < src_refs->len; i++)
    {
      const char *src_ref = g_ptr_array_index (src_refs, i);
      char *resolved_ref;

      if (!ostree_repo_resolve_rev (src_repo, src_ref, FALSE, &resolved_ref, error))
        return FALSE;

      g_ptr_array_add (resolved_src_refs, resolved_ref);
    }

  if (src_repo_uri != NULL)
    {
      OstreeRepoPullFlags pullflags = 0;
      GVariantBuilder builder;
      g_autoptr(OstreeAsyncProgress) progress = NULL;
      g_auto(GLnxConsoleRef) console = { 0, };
      g_autoptr(GVariant) options = NULL;
      gboolean res;

      if (opt_untrusted)
        pullflags |= OSTREE_REPO_PULL_FLAGS_UNTRUSTED;

      glnx_console_lock (&console);
      if (console.is_tty)
        progress = ostree_async_progress_new_and_connect (ostree_repo_pull_default_console_progress_changed, &console);

      g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}"));
      g_variant_builder_add (&builder, "{s@v}", "flags",
                             g_variant_new_variant (g_variant_new_int32 (pullflags)));
      g_variant_builder_add (&builder, "{s@v}", "refs",
                             g_variant_new_variant (g_variant_new_strv ((const char * const *) resolved_src_refs->pdata,
                                                                        resolved_src_refs->len)));
      g_variant_builder_add (&builder, "{s@v}", "depth",
                             g_variant_new_variant (g_variant_new_int32 (0)));

      options = g_variant_ref_sink (g_variant_builder_end (&builder));
      res = ostree_repo_pull_with_options (dst_repo, src_repo_uri,
                                           options,
                                           progress,
                                           cancellable, error);

      if (progress)
        ostree_async_progress_finish (progress);

      if (!res)
        return FALSE;
    }

  /* By now we have the commit with commit_id==resolved_ref and dependencies in dst_repo. We now create a new
   * commit based on the toplevel tree ref from that commit.
   * This is equivalent to:
   *   ostree commit --skip-if-unchanged --repo=${destrepo} --tree=ref=${resolved_ref}
   */

  transaction = flatpak_repo_transaction_start (dst_repo, cancellable, error);
  if (transaction == NULL)
    return FALSE;

  for (i = 0; i < resolved_src_refs->len; i++)
    {
      const char *dst_ref = dst_refs[i];
      const char *resolved_ref = g_ptr_array_index (resolved_src_refs, i);
      g_autofree char *dst_parent = NULL;
      g_autoptr(GFile) dst_parent_root = NULL;
      g_autoptr(GFile) src_ref_root = NULL;
      g_autoptr(GVariant) src_commitv = NULL;
      g_autoptr(GVariant) dst_commitv = NULL;
      g_autoptr(OstreeMutableTree) mtree = NULL;
      g_autoptr(GFile) dst_root = NULL;
      g_autoptr(GVariant) commitv_metadata = NULL;
      g_autoptr(GVariant) metadata = NULL;
      const char *subject;
      const char *body;
      g_autofree char *commit_checksum = NULL;
      GVariantBuilder metadata_builder;
      gint j;
      const char *dst_collection_id = NULL;
      const char *main_collection_id = NULL;
      g_autoptr(GPtrArray) collection_ids = NULL;

      if (!ostree_repo_resolve_rev (dst_repo, dst_ref, TRUE, &dst_parent, error))
        return FALSE;

      if (dst_parent != NULL &&
          !ostree_repo_read_commit (dst_repo, dst_parent, &dst_parent_root, NULL, cancellable, error))
        return FALSE;

      if (!ostree_repo_read_commit (dst_repo, resolved_ref, &src_ref_root, NULL, cancellable, error))
        return FALSE;

      if (!ostree_repo_load_commit (dst_repo, resolved_ref, &src_commitv, &src_commit_state, error))
        return FALSE;

      if (src_commit_state & OSTREE_REPO_COMMIT_STATE_PARTIAL)
        return flatpak_fail (error, _("Can't commit from partial source commit."));

      /* Don't create a new commit if this is the same tree */
      if (!opt_force && dst_parent_root != NULL && g_file_equal (dst_parent_root, src_ref_root))
        {
          g_print (_("%s: no change\n"), dst_ref);
          continue;
        }

      mtree = ostree_mutable_tree_new ();
      if (!ostree_repo_write_directory_to_mtree (dst_repo, src_ref_root, mtree, NULL,
                                                 cancellable, error))
        return FALSE;

      if (!ostree_repo_write_mtree (dst_repo, mtree, &dst_root, cancellable, error))
        return FALSE;

      commitv_metadata = g_variant_get_child_value (src_commitv, 0);

      g_variant_get_child (src_commitv, 3, "&s", &subject);
      if (opt_subject)
        subject = (const char *) opt_subject;
      g_variant_get_child (src_commitv, 4, "&s", &body);
      if (opt_body)
        body = (const char *) opt_body;

      dst_collection_id = ostree_repo_get_collection_id (dst_repo);

      collection_ids = g_ptr_array_new_with_free_func (g_free);
      if (dst_collection_id)
        {
          main_collection_id = dst_collection_id;
          g_ptr_array_add (collection_ids, g_strdup (dst_collection_id));
        }

      if (opt_extra_collection_ids != NULL)
        {
          for (j = 0; opt_extra_collection_ids[j] != NULL; j++)
            {
              const char *cid = opt_extra_collection_ids[j];
              if (main_collection_id == NULL)
                main_collection_id = cid; /* Fall back to first arg */

              if (g_strcmp0 (cid, dst_collection_id) != 0)
                g_ptr_array_add (collection_ids, g_strdup (cid));
            }
        }

      g_ptr_array_sort (collection_ids, (GCompareFunc) flatpak_strcmp0_ptr);

      /* Copy old metadata */
      g_variant_builder_init (&metadata_builder, G_VARIANT_TYPE ("a{sv}"));

      /* Bindings. xa.ref is deprecated but added anyway for backwards compatibility. */
      g_variant_builder_add (&metadata_builder, "{sv}", "ostree.collection-binding",
                             g_variant_new_string (main_collection_id ? main_collection_id : ""));
      if (collection_ids->len > 0)
        {
          g_autoptr(GVariantBuilder) cr_builder = g_variant_builder_new (G_VARIANT_TYPE ("a(ss)"));

          for (j = 0; j < collection_ids->len; j++)
            g_variant_builder_add (cr_builder, "(ss)", g_ptr_array_index (collection_ids, j), dst_ref);

          g_variant_builder_add (&metadata_builder, "{sv}", "ostree.collection-refs-binding",
                                 g_variant_builder_end (cr_builder));
        }
      g_variant_builder_add (&metadata_builder, "{sv}", "ostree.ref-binding",
                             g_variant_new_strv (&dst_ref, 1));
      g_variant_builder_add (&metadata_builder, "{sv}", "xa.ref", g_variant_new_string (dst_ref));

      /* Record the source commit. This is nice to have, but it also
         means the commit-from gets a different commit id, which
         avoids problems with e.g.  sharing .commitmeta files
         (signatures) */
      g_variant_builder_add (&metadata_builder, "{sv}", "xa.from_commit", g_variant_new_string (resolved_ref));

      for (j = 0; j < g_variant_n_children (commitv_metadata); j++)
        {
          g_autoptr(GVariant) child = g_variant_get_child_value (commitv_metadata, j);
          g_autoptr(GVariant) keyv = g_variant_get_child_value (child, 0);
          const char *key = g_variant_get_string (keyv, NULL);

          if (strcmp (key, "xa.ref") == 0 ||
              strcmp (key, "xa.from_commit") == 0 ||
              strcmp (key, "ostree.collection-binding") == 0 ||
              strcmp (key, "ostree.collection-refs-binding") == 0 ||
              strcmp (key, "ostree.ref-binding") == 0)
            continue;

          if (opt_endoflife &&
              strcmp (key, OSTREE_COMMIT_META_KEY_ENDOFLIFE) == 0)
            continue;

          if (opt_endoflife_rebase &&
              strcmp (key, OSTREE_COMMIT_META_KEY_ENDOFLIFE_REBASE) == 0)
            continue;

          g_variant_builder_add_value (&metadata_builder, child);
        }

      if (opt_endoflife && *opt_endoflife)
        g_variant_builder_add (&metadata_builder, "{sv}", OSTREE_COMMIT_META_KEY_ENDOFLIFE,
                               g_variant_new_string (opt_endoflife));

      if (opt_endoflife_rebase)
        {
          g_auto(GStrv) dst_ref_parts = g_strsplit (dst_ref, "/", 0);

          for (j = 0; opt_endoflife_rebase[j] != NULL; j++)
            {
              const char *old_prefix = opt_endoflife_rebase[j];

              if (flatpak_has_name_prefix (dst_ref_parts[1], old_prefix))
                {
                  g_autofree char *new_id = g_strconcat (opt_endoflife_rebase_new[j], dst_ref_parts[1] + strlen(old_prefix), NULL);
                  g_autofree char *rebased_ref = g_build_filename (dst_ref_parts[0], new_id, dst_ref_parts[2], dst_ref_parts[3], NULL);

                  g_variant_builder_add (&metadata_builder, "{sv}", OSTREE_COMMIT_META_KEY_ENDOFLIFE_REBASE,
                                         g_variant_new_string (rebased_ref));
                  break;
                }
            }
        }

      timestamp = ostree_commit_get_timestamp (src_commitv);
      if (opt_timestamp)
        timestamp = ts.tv_sec;

      metadata = g_variant_ref_sink (g_variant_builder_end (&metadata_builder));
      if (!ostree_repo_write_commit_with_time (dst_repo, dst_parent, subject, body, metadata,
                                               OSTREE_REPO_FILE (dst_root),
                                               timestamp,
                                               &commit_checksum, cancellable, error))
        return FALSE;

      g_print ("%s: %s\n", dst_ref, commit_checksum);

      if (!ostree_repo_load_commit (dst_repo, commit_checksum, &dst_commitv, NULL, error))
        return FALSE;

      /* This doesn't copy the detached metadata. I'm not sure if this is a problem.
       * The main thing there is commit signatures, and we can't copy those, as the commit hash changes.
       */

      if (opt_gpg_key_ids)
        {
          char **iter;

          for (iter = opt_gpg_key_ids; iter && *iter; iter++)
            {
              const char *keyid = *iter;
              g_autoptr(GError) my_error = NULL;

              if (!ostree_repo_sign_commit (dst_repo,
                                            commit_checksum,
                                            keyid,
                                            opt_gpg_homedir,
                                            cancellable,
                                            &my_error) &&
                  !g_error_matches (my_error, G_IO_ERROR, G_IO_ERROR_EXISTS))
                {
                  g_propagate_error (error, g_steal_pointer (&my_error));
                  return FALSE;
                }
            }
        }

      if (dst_collection_id != NULL)
        {
          OstreeCollectionRef ref = { (char *) dst_collection_id, (char *) dst_ref };
          ostree_repo_transaction_set_collection_ref (dst_repo, &ref, commit_checksum);
        }
      else
        {
          ostree_repo_transaction_set_ref (dst_repo, NULL, dst_ref, commit_checksum);
        }

      if (opt_extra_collection_ids)
        {
          for (j = 0; opt_extra_collection_ids[j] != NULL; j++)
            {
              OstreeCollectionRef ref = { (char *) opt_extra_collection_ids[j], (char *) dst_ref };
              ostree_repo_transaction_set_collection_ref (dst_repo, &ref, commit_checksum);
            }
        }

      /* Copy + Rewrite any deltas */
      {
        const char *from[2];
        gsize j, n_from = 0;

        if (dst_parent != NULL)
          from[n_from++] = dst_parent;
        from[n_from++] = NULL;

        for (j = 0; j < n_from; j++)
          {
            g_autoptr(GError) local_error = NULL;
            if (!rewrite_delta (src_repo, resolved_ref, dst_repo, commit_checksum, dst_commitv, from[j], &local_error))
              g_debug ("Failed to copy delta: %s", local_error->message);
          }
      }
    }

  if (!ostree_repo_commit_transaction (dst_repo, NULL, cancellable, error))
    return FALSE;

  if (opt_update_appstream &&
      !flatpak_repo_generate_appstream (dst_repo, (const char **) opt_gpg_key_ids, opt_gpg_homedir, 0, cancellable, error))
    return FALSE;

  if (!opt_no_update_summary &&
      !flatpak_repo_update (dst_repo,
                            (const char **) opt_gpg_key_ids,
                            opt_gpg_homedir,
                            cancellable,
                            error))
    return FALSE;

  return TRUE;
}
Example #6
0
gboolean
ostree_builtin_pull (int argc, char **argv, GCancellable *cancellable, GError **error)
{
  GOptionContext *context;
  gs_unref_object OstreeRepo *repo = NULL;
  gboolean ret = FALSE;
  gs_free char *remote = NULL;
  OstreeRepoPullFlags pullflags = 0;
  GSConsole *console = NULL;
  gs_unref_ptrarray GPtrArray *refs_to_fetch = NULL;
  gs_unref_object OstreeAsyncProgress *progress = NULL;

  context = g_option_context_new ("REMOTE [BRANCH...] - Download data from remote repository");

  if (!ostree_option_context_parse (context, options, &argc, &argv, OSTREE_BUILTIN_FLAG_NONE, &repo, cancellable, error))
    goto out;

  if (argc < 2)
    {
      ot_util_usage_error (context, "REMOTE must be specified", error);
      goto out;
    }

  if (opt_disable_fsync)
    ostree_repo_set_disable_fsync (repo, TRUE);

  if (opt_mirror)
    pullflags |= OSTREE_REPO_PULL_FLAGS_MIRROR;

  if (strchr (argv[1], ':') == NULL)
    {
      remote = g_strdup (argv[1]);
      if (argc > 2)
        {
          int i;
          refs_to_fetch = g_ptr_array_new ();
          for (i = 2; i < argc; i++)
            g_ptr_array_add (refs_to_fetch, argv[i]);
          g_ptr_array_add (refs_to_fetch, NULL);
        }
    }
  else
    {
      char *ref_to_fetch;
      refs_to_fetch = g_ptr_array_new ();
      if (!ostree_parse_refspec (argv[1], &remote, &ref_to_fetch, error))
        goto out;
      /* Transfer ownership */
      g_ptr_array_add (refs_to_fetch, ref_to_fetch);
      g_ptr_array_add (refs_to_fetch, NULL);
    }

  console = gs_console_get ();
  if (console)
    {
      gs_console_begin_status_line (console, "", NULL, NULL);
      progress = ostree_async_progress_new_and_connect (ostree_repo_pull_default_console_progress_changed, console);
    }

  {
    GVariantBuilder builder;
    g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}"));

    if (opt_subpath)
      g_variant_builder_add (&builder, "{s@v}", "subdir",
                             g_variant_new_variant (g_variant_new_string (opt_subpath)));
    g_variant_builder_add (&builder, "{s@v}", "flags",
                           g_variant_new_variant (g_variant_new_int32 (pullflags)));
    if (refs_to_fetch)
      g_variant_builder_add (&builder, "{s@v}", "refs",
                             g_variant_new_variant (g_variant_new_strv ((const char *const*) refs_to_fetch->pdata, -1)));
    g_variant_builder_add (&builder, "{s@v}", "depth",
                           g_variant_new_variant (g_variant_new_int32 (opt_depth)));
    
    if (!ostree_repo_pull_with_options (repo, remote, g_variant_builder_end (&builder),
                                        progress, cancellable, error))
      goto out;
  }

  if (progress)
    ostree_async_progress_finish (progress);

  ret = TRUE;
 out:
  if (console)
    gs_console_end_status_line (console, NULL, NULL);
 
  if (context)
    g_option_context_free (context);
  return ret;
}
gboolean
ostree_builtin_pull_local (int argc, char **argv, OstreeCommandInvocation *invocation, GCancellable *cancellable, GError **error)
{
  gboolean ret = FALSE;
  g_autoptr(GOptionContext) context = NULL;
  g_autoptr(OstreeRepo) repo = NULL;
  int i;
  const char *src_repo_arg;
  g_autofree char *src_repo_uri = NULL;
  g_autoptr(OstreeAsyncProgress) progress = NULL;
  g_autoptr(GPtrArray) refs_to_fetch = NULL;
  OstreeRepoPullFlags pullflags = 0;

  context = g_option_context_new ("SRC_REPO [REFS...]");

  if (!ostree_option_context_parse (context, options, &argc, &argv, invocation, &repo, cancellable, error))
    goto out;

  if (!ostree_ensure_repo_writable (repo, error))
    goto out;

  if (argc < 2)
    {
      gchar *help = g_option_context_get_help (context, TRUE, NULL);
      g_printerr ("%s\n", help);
      g_free (help);
      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                               "DESTINATION must be specified");
      goto out;
    }

  src_repo_arg = argv[1];

  if (src_repo_arg[0] == '/')
    src_repo_uri = g_strconcat ("file://", src_repo_arg, NULL);
  else
    { 
      g_autofree char *cwd = g_get_current_dir ();
      src_repo_uri = g_strconcat ("file://", cwd, "/", src_repo_arg, NULL);
    }

  if (opt_untrusted)
    pullflags |= OSTREE_REPO_PULL_FLAGS_UNTRUSTED;
  if (opt_bareuseronly_files)
    pullflags |= OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES;

  if (opt_disable_fsync)
    ostree_repo_set_disable_fsync (repo, TRUE);

  if (argc == 2)
    {
      g_autoptr(GFile) src_repo_path = g_file_new_for_path (src_repo_arg);
      g_autoptr(OstreeRepo) src_repo = ostree_repo_new (src_repo_path);
      g_autoptr(GHashTable) refs_to_clone = NULL;

      refs_to_fetch = g_ptr_array_new_with_free_func (g_free);

      if (!ostree_repo_open (src_repo, cancellable, error))
        goto out;

      /* FIXME: This should grow support for pulling refs from refs/mirrors on
       * a local repository, using ostree_repo_list_collection_refs(). */
      if (!ostree_repo_list_refs (src_repo, NULL, &refs_to_clone,
                                  cancellable, error))
        goto out;

      { GHashTableIter hashiter;
        gpointer hkey, hvalue;
        
        g_hash_table_iter_init (&hashiter, refs_to_clone);
        while (g_hash_table_iter_next (&hashiter, &hkey, &hvalue))
          g_ptr_array_add (refs_to_fetch, g_strdup (hkey));
      }
      g_ptr_array_add (refs_to_fetch, NULL);
    }
  else
    {
      refs_to_fetch = g_ptr_array_new ();
      for (i = 2; i < argc; i++)
        {
          const char *ref = argv[i];
          
          g_ptr_array_add (refs_to_fetch, (char*)ref);
        }
      g_ptr_array_add (refs_to_fetch, NULL);
    }

  { GVariantBuilder builder;
    g_autoptr(GVariant) opts = NULL;
    g_auto(GLnxConsoleRef) console = { 0, };

    glnx_console_lock (&console);

    g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}"));

    g_variant_builder_add (&builder, "{s@v}", "flags",
                           g_variant_new_variant (g_variant_new_int32 (pullflags)));
    g_variant_builder_add (&builder, "{s@v}", "refs",
                           g_variant_new_variant (g_variant_new_strv ((const char *const*) refs_to_fetch->pdata, -1)));
    if (opt_remote)
      g_variant_builder_add (&builder, "{s@v}", "override-remote-name",
                             g_variant_new_variant (g_variant_new_string (opt_remote)));
    g_variant_builder_add (&builder, "{s@v}", "require-static-deltas",
                           g_variant_new_variant (g_variant_new_boolean (opt_require_static_deltas)));
    if (opt_gpg_verify)
      g_variant_builder_add (&builder, "{s@v}", "gpg-verify",
                             g_variant_new_variant (g_variant_new_boolean (TRUE)));
    if (opt_gpg_verify_summary)
      g_variant_builder_add (&builder, "{s@v}", "gpg-verify-summary",
                             g_variant_new_variant (g_variant_new_boolean (TRUE)));
    g_variant_builder_add (&builder, "{s@v}", "depth",
                           g_variant_new_variant (g_variant_new_int32 (opt_depth)));

    if (console.is_tty)
      progress = ostree_async_progress_new_and_connect (ostree_repo_pull_default_console_progress_changed, &console);
    else
      progress = ostree_async_progress_new_and_connect (noninteractive_console_progress_changed, &console);

    opts = g_variant_ref_sink (g_variant_builder_end (&builder));
    if (!ostree_repo_pull_with_options (repo, src_repo_uri, 
                                        opts,
                                        progress,
                                        cancellable, error))
      goto out;

    if (!console.is_tty)
      {
        g_assert (progress);
        const char *status = ostree_async_progress_get_status (progress);
        if (status)
          g_print ("%s\n", status);
      }
    ostree_async_progress_finish (progress);
  }

  ret = TRUE;
 out:
  if (repo)
    ostree_repo_abort_transaction (repo, cancellable, NULL);
  return ret;
}
/* TODO: Add a man page. */
gboolean
ostree_builtin_create_usb (int            argc,
                           char         **argv,
                           OstreeCommandInvocation *invocation,
                           GCancellable  *cancellable,
                           GError       **error)
{
  g_autoptr(GOptionContext) context = NULL;
  g_autoptr(OstreeAsyncProgress) progress = NULL;
  g_auto(GLnxConsoleRef) console = { 0, };

  context = g_option_context_new ("MOUNT-PATH COLLECTION-ID REF [COLLECTION-ID REF...]");

  /* Parse options. */
  g_autoptr(OstreeRepo) src_repo = NULL;

  if (!ostree_option_context_parse (context, options, &argc, &argv, invocation, &src_repo, cancellable, error))
    return FALSE;

  if (argc < 2)
    {
      ot_util_usage_error (context, "A MOUNT-PATH must be specified", error);
      return FALSE;
    }

  if (argc < 4)
    {
      ot_util_usage_error (context, "At least one COLLECTION-ID REF pair must be specified", error);
      return FALSE;
    }

  if (argc % 2 == 1)
    {
      ot_util_usage_error (context, "Only complete COLLECTION-ID REF pairs may be specified", error);
      return FALSE;
    }

  /* Open the USB stick, which must exist. Allow automounting and following symlinks. */
  const char *mount_root_path = argv[1];
  struct stat mount_root_stbuf;

  glnx_autofd int mount_root_dfd = -1;
  if (!glnx_opendirat (AT_FDCWD, mount_root_path, TRUE, &mount_root_dfd, error))
    return FALSE;
  if (!glnx_fstat (mount_root_dfd, &mount_root_stbuf, error))
    return FALSE;

  /* Read in the refs to add to the USB stick. */
  g_autoptr(GPtrArray) refs = g_ptr_array_new_full (argc, (GDestroyNotify) ostree_collection_ref_free);

  for (gsize i = 2; i < argc; i += 2)
    {
      if (!ostree_validate_collection_id (argv[i], error) ||
          !ostree_validate_rev (argv[i + 1], error))
        return FALSE;

      g_ptr_array_add (refs, ostree_collection_ref_new (argv[i], argv[i + 1]));
    }

  /* Open the destination repository on the USB stick or create it if it doesn’t exist.
   * Check it’s below @mount_root_path, and that it’s not the same as the source
   * repository.
   *
   * If the destination file system supports xattrs (for example, ext4), we use
   * a BARE_USER repository; if it doesn’t (for example, FAT), we use ARCHIVE.
   * In either case, we want a lossless repository. */
  const char *dest_repo_path = (opt_destination_repo != NULL) ? opt_destination_repo : ".ostree/repo";

  if (!glnx_shutil_mkdir_p_at (mount_root_dfd, dest_repo_path, 0755, cancellable, error))
    return FALSE;

  OstreeRepoMode mode = OSTREE_REPO_MODE_BARE_USER;

  if (TEMP_FAILURE_RETRY (fgetxattr (mount_root_dfd, "user.test", NULL, 0)) < 0 &&
      (errno == ENOTSUP || errno == EOPNOTSUPP))
    mode = OSTREE_REPO_MODE_ARCHIVE;

  g_debug ("%s: Creating repository in mode %u", G_STRFUNC, mode);

  g_autoptr(OstreeRepo) dest_repo = ostree_repo_create_at (mount_root_dfd, dest_repo_path,
                                                           mode, NULL, cancellable, error);

  if (dest_repo == NULL)
    return FALSE;

  struct stat dest_repo_stbuf;

  if (!glnx_fstat (ostree_repo_get_dfd (dest_repo), &dest_repo_stbuf, error))
    return FALSE;

  if (dest_repo_stbuf.st_dev != mount_root_stbuf.st_dev)
    {
      ot_util_usage_error (context, "--destination-repo must be a descendent of MOUNT-PATH", error);
      return FALSE;
    }

  if (ostree_repo_equal (src_repo, dest_repo))
    {
      ot_util_usage_error (context, "--destination-repo must not be the source repository", error);
      return FALSE;
    }

  if (!ostree_ensure_repo_writable (dest_repo, error))
    return FALSE;

  if (opt_disable_fsync)
    ostree_repo_set_disable_fsync (dest_repo, TRUE);

  /* Copy across all of the collection–refs to the destination repo. */
  GVariantBuilder refs_builder;
  g_variant_builder_init (&refs_builder, G_VARIANT_TYPE ("a(sss)"));

  for (gsize i = 0; i < refs->len; i++)
    {
      const OstreeCollectionRef *ref = g_ptr_array_index (refs, i);

      g_variant_builder_add (&refs_builder, "(sss)",
                             ref->collection_id, ref->ref_name, "");
    }

  {
    GVariantBuilder builder;
    g_autoptr(GVariant) opts = NULL;
    OstreeRepoPullFlags flags = OSTREE_REPO_PULL_FLAGS_MIRROR;

    glnx_console_lock (&console);

    if (console.is_tty)
      progress = ostree_async_progress_new_and_connect (ostree_repo_pull_default_console_progress_changed, &console);

    g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}"));

    g_variant_builder_add (&builder, "{s@v}", "collection-refs",
                           g_variant_new_variant (g_variant_builder_end (&refs_builder)));
    g_variant_builder_add (&builder, "{s@v}", "flags",
                           g_variant_new_variant (g_variant_new_int32 (flags)));
    g_variant_builder_add (&builder, "{s@v}", "depth",
                           g_variant_new_variant (g_variant_new_int32 (0)));
    opts = g_variant_ref_sink (g_variant_builder_end (&builder));

    g_autofree char *src_repo_uri = g_file_get_uri (ostree_repo_get_path (src_repo));

    if (!ostree_repo_pull_with_options (dest_repo, src_repo_uri,
                                        opts,
                                        progress,
                                        cancellable, error))
      {
        ostree_repo_abort_transaction (dest_repo, cancellable, NULL);
        return FALSE;
      }

    if (progress != NULL)
      ostree_async_progress_finish (progress);
  }

  /* Ensure a summary file is present to make it easier to look up commit checksums. */
  /* FIXME: It should be possible to work without this, but find_remotes_cb() in
   * ostree-repo-pull.c currently assumes a summary file (signed or unsigned) is
   * present. */
  struct stat stbuf;
  if (!glnx_fstatat_allow_noent (ostree_repo_get_dfd (dest_repo), "summary", &stbuf, 0, error))
    return FALSE;
  if (errno == ENOENT &&
      !ostree_repo_regenerate_summary (dest_repo, NULL, cancellable, error))
    return FALSE;

  /* Add the symlinks .ostree/repos.d/@symlink_name → @dest_repo_path, unless
   * the @dest_repo_path is a well-known one like ostree/repo, in which case no
   * symlink is necessary; #OstreeRepoFinderMount always looks there. */
  if (!g_str_equal (dest_repo_path, "ostree/repo") &&
      !g_str_equal (dest_repo_path, ".ostree/repo"))
    {
      if (!glnx_shutil_mkdir_p_at (mount_root_dfd, ".ostree/repos.d", 0755, cancellable, error))
        return FALSE;

      /* Find a unique name for the symlink. If a symlink already targets
       * @dest_repo_path, use that and don’t create a new one. */
      GLnxDirFdIterator repos_iter;
      gboolean need_symlink = TRUE;

      if (!glnx_dirfd_iterator_init_at (mount_root_dfd, ".ostree/repos.d", TRUE, &repos_iter, error))
        return FALSE;

      while (TRUE)
        {
          struct dirent *repo_dent;

          if (!glnx_dirfd_iterator_next_dent (&repos_iter, &repo_dent, cancellable, error))
            return FALSE;

          if (repo_dent == NULL)
            break;

          /* Does the symlink already point to this repository? (Or is the
           * repository itself present in repos.d?) We already guarantee that
           * they’re on the same device. */
          if (repo_dent->d_ino == dest_repo_stbuf.st_ino)
            {
              need_symlink = FALSE;
              break;
            }
        }

      /* If we need a symlink, find a unique name for it and create it. */
      if (need_symlink)
        {
          /* Relative to .ostree/repos.d. */
          g_autofree char *relative_dest_repo_path = g_build_filename ("..", "..", dest_repo_path, NULL);
          guint i;
          const guint max_attempts = 100;

          for (i = 0; i < max_attempts; i++)
            {
              g_autofree char *symlink_path = g_strdup_printf (".ostree/repos.d/%02u-generated", i);

              int ret = TEMP_FAILURE_RETRY (symlinkat (relative_dest_repo_path, mount_root_dfd, symlink_path));
              if (ret < 0 && errno != EEXIST)
                return glnx_throw_errno_prefix (error, "symlinkat(%s → %s)", symlink_path, relative_dest_repo_path);
              else if (ret >= 0)
                break;
            }

          if (i == max_attempts)
            return glnx_throw (error, "Could not find an unused symlink name for the repository");
        }
    }

  /* Report success to the user. */
  g_autofree char *src_repo_path = g_file_get_path (ostree_repo_get_path (src_repo));

  g_print ("Copied %u/%u refs successfully from ‘%s’ to ‘%s’ repository in ‘%s’.\n", refs->len, refs->len,
           src_repo_path, dest_repo_path, mount_root_path);

  return TRUE;
}