/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s:
 * Copyright © 2016 Red Hat, Inc
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *       Alexander Larsson <alexl@redhat.com>
 */

#include "config.h"

#include <stdio.h>
#include <glib/gi18n-lib.h>

#include "flatpak-auth-private.h"
#include "flatpak-error.h"
#include "flatpak-installation-private.h"
#include "flatpak-progress-private.h"
#include "flatpak-transaction-private.h"
#include "flatpak-utils-private.h"
#include "flatpak-uri-private.h"
#include "flatpak-variant-impl-private.h"

/**
 * SECTION:flatpak-transaction
 * @Title: FlatpakTransaction
 * @Short_description: Transaction information
 *
 * FlatpakTransaction is an object representing an install/update/uninstall
 * transaction. You create an object like this using flatpak_transaction_new_for_installation()
 * and then you add all the operations (installs, updates, etc) you wish to do. Then
 * you start the transaction with flatpak_transaction_run() which will resolve all kinds
 * of dependencies and report progress and status while downloading and installing these.
 *
 * The dependency resolution that is the first step of executing a transaction can
 * be influenced by flatpak_transaction_set_disable_dependencies(),
 * flatpak_transaction_set_disable_related(), flatpak_transaction_add_dependency_source()
 * and flatpak_transaction_add_default_dependency_sources().
 *
 * The underlying operations that get orchestrated by a FlatpakTransaction are: pulling
 * new data from remote repositories, deploying newer applications or runtimes and pruning
 * old deployments. Which of these operations are carried out can be controlled with
 * flatpak_transaction_set_no_pull(), flatpak_transaction_set_no_deploy() and
 * flatpak_transaction_set_disable_prune().
 *
 * A transaction is a blocking operation, and all signals are emitted in the same thread.
 * This means you should either handle the signals directly (say, by doing blocking console
 * interaction, or by just returning without interaction), or run the operation in a separate
 * thread and do your own forwarding to the GUI thread.
 *
 * Despite the name, a FlatpakTransaction is more like a batch operation than a transaction
 * in the database sense. Individual operations are carried out sequentially, and are atomic.
 * They become visible to the system as they are completed. When an error occurs, already
 * completed operations are not rolled back.
 *
 * For each operation that is executed during a transaction, you first get a
 * #FlatpakTransaction::new-operation signal, followed by either a
 * #FlatpakTransaction::operation-done or #FlatpakTransaction::operation-error.

 * The FlatpakTransaction API is threadsafe in the sense that it is safe to run two
 * transactions at the same time, in different threads (or processes).
 *
 * Note: Transactions (or any other install/update operation) to a
 * system installation rely on the ability to create files that are readable
 * by other users. Some users set a umask that prohibits this. Unfortunately
 * there is no good way to work around this in a threadsafe, local way, so
 * such setups will break by default. The flatpak commandline app works
 * around this by calling umask(022) in the early setup, and it is recommended
 * that other apps using libflatpak do this too.
 */

/* This is an internal-only element of FlatpakTransactionOperationType */
#define FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE FLATPAK_TRANSACTION_OPERATION_LAST_TYPE + 1

enum {
  RUNTIME_UPDATE,
  RUNTIME_INSTALL,
  APP_UPDATE,
  APP_INSTALL
};

struct _FlatpakTransactionOperation
{
  GObject                         parent;

  char                           *remote;
  FlatpakDecomposed              *ref;
  /* NULL means unspecified (normally keep whatever was there before), [] means force everything */
  char                          **subpaths;
  char                          **previous_ids;
  char                           *commit;
  GFile                          *bundle;
  GBytes                         *external_metadata;
  FlatpakTransactionOperationType kind;
  gboolean                        non_fatal;
  gboolean                        failed;
  gboolean                        skip;
  gboolean                        update_only_deploy;
  gboolean                        pin_on_deploy;

  gboolean                        resolved;
  char                           *resolved_commit;
  GFile                          *resolved_sideload_path;
  GBytes                         *resolved_metadata;
  GKeyFile                       *resolved_metakey;
  GBytes                         *resolved_old_metadata;
  GKeyFile                       *resolved_old_metakey;
  char                           *resolved_token;
  gboolean                        requested_token; /* TRUE if we requested a token. value in resolved_token, but may be NULL if token not needed. */
  guint64                         download_size;
  guint64                         installed_size;
  char                           *eol;
  char                           *eol_rebase;
  gint32                          token_type;
  GVariant                       *summary_metadata; /* Additional metadatafield for commit from summary */
  int                             run_after_count;
  int                             run_after_prio; /* Higher => run later (when it becomes runnable). Used to run related ops (runtime extensions) before deps (apps using the runtime) */
  GList                          *run_before_ops;
  gboolean                        run_last;  /* Run this after all the other apps that are not run_last */
  FlatpakTransactionOperation    *fail_if_op_fails; /* main app/runtime for related extensions, runtime for apps, install/update for uninstalls of eol-rebase apps */
  /* main app/runtime for related extensions, app for runtimes; could be multiple
   * related-to-ops if this op is for a runtime which is needed by multiple apps
   * in the transaction: */
  GPtrArray                      *related_to_ops;  /* (element-type FlatpakTransactionOperation) (nullable) */
};

typedef struct _FlatpakTransactionPrivate FlatpakTransactionPrivate;

typedef struct _BundleData                BundleData;

struct _BundleData
{
  GFile  *file;
  GBytes *gpg_data;
};

typedef struct {
  FlatpakTransaction *transaction;
  const char *remote;
  FlatpakAuthenticatorRequest *request;
  gboolean done;
  guint response;
  GVariant *results;
} RequestData;

struct _FlatpakTransactionPrivate
{
  GObject                      parent;

  FlatpakInstallation         *installation;
  FlatpakDir                  *dir;
  GHashTable                  *last_op_for_ref;
  GHashTable                  *remote_states; /* (element-type utf8 FlatpakRemoteState) */
  GPtrArray                   *extra_dependency_dirs;
  GPtrArray                   *extra_sideload_repos;
  GList                       *ops;
  GPtrArray                   *added_origin_remotes;

  GList                       *flatpakrefs; /* GKeyFiles */
  GList                       *bundles; /* BundleData */

  guint                        next_request_id;
  guint                        active_request_id;
  RequestData                 *active_request;

  FlatpakTransactionOperation *current_op;

  char                        *parent_window;
  gboolean                     no_pull;
  gboolean                     no_deploy;
  gboolean                     disable_auto_pin;
  gboolean                     disable_static_deltas;
  gboolean                     disable_prune;
  gboolean                     disable_deps;
  gboolean                     disable_related;
  gboolean                     reinstall;
  gboolean                     force_uninstall;
  gboolean                     can_run;
  gboolean                     include_unused_uninstall_ops;
  gboolean                     auto_install_sdk;
  gboolean                     auto_install_debug;
  char                        *default_arch;
  guint                        max_op;

  gboolean                     needs_resolve;
  gboolean                     needs_tokens;
};

enum {
  NEW_OPERATION,
  OPERATION_DONE,
  OPERATION_ERROR,
  CHOOSE_REMOTE_FOR_REF,
  END_OF_LIFED,
  END_OF_LIFED_WITH_REBASE,
  READY,
  READY_PRE_AUTH,
  ADD_NEW_REMOTE,
  WEBFLOW_START,
  WEBFLOW_DONE,
  BASIC_AUTH_START,
  INSTALL_AUTHENTICATOR,
  LAST_SIGNAL
};

typedef enum {
  PROP_INSTALLATION = 1,
  PROP_NO_INTERACTION,
} FlatpakTransactionProperty;

struct _FlatpakTransactionProgress
{
  GObject              parent;

  FlatpakProgress     *progress_obj;
};

enum {
  CHANGED,
  LAST_PROGRESS_SIGNAL
};

static gboolean op_may_need_token (FlatpakTransactionOperation *op);

static void flatpak_transaction_normalize_ops (FlatpakTransaction *self);
static gboolean request_required_tokens (FlatpakTransaction *self,
                                         const char         *optional_remote,
                                         GCancellable       *cancellable,
                                         GError            **error);


static BundleData *
bundle_data_new (GFile  *file,
                 GBytes *gpg_data)
{
  BundleData *data = g_new0 (BundleData, 1);

  data->file = g_object_ref (file);
  if (gpg_data)
    data->gpg_data = g_bytes_ref (gpg_data);

  return data;
}

static void
bundle_data_free (BundleData *data)
{
  g_clear_object (&data->file);
  g_clear_object (&data->gpg_data);
  g_free (data);
}

static guint progress_signals[LAST_SIGNAL] = { 0 };

/**
 * SECTION:flatpak-transaction-progress
 * @Title: FlatpakTransactionProgress
 * @Short_description: Progress of an operation
 *
 * FlatpakTransactionProgress is an object that represents the progress
 * of a single operation in a transaction. You obtain a FlatpakTransactionProgress
 * with the #FlatpakTransaction::new-operation signal.
 */

G_DEFINE_TYPE (FlatpakTransactionProgress, flatpak_transaction_progress, G_TYPE_OBJECT)

/**
 * flatpak_transaction_progress_set_update_frequency:
 * @self: a #FlatpakTransactionProgress
 * @update_interval: the update interval, in milliseconds
 *
 * Sets how often progress should be updated.
 */
void
flatpak_transaction_progress_set_update_frequency (FlatpakTransactionProgress *self,
                                                   guint                       update_interval)
{
  flatpak_progress_set_update_interval (self->progress_obj, update_interval);
}

/**
 * flatpak_transaction_progress_get_status:
 * @self: a #FlatpakTransactionProgress
 *
 * Gets the current status string
 *
 * Returns: (transfer full): the current status
 */
char *
flatpak_transaction_progress_get_status (FlatpakTransactionProgress *self)
{
  return g_strdup (flatpak_progress_get_status (self->progress_obj));
}

/**
 * flatpak_transaction_progress_get_is_estimating:
 * @self: a #FlatpakTransactionProgress
 *
 * Gets whether the progress is currently estimating
 *
 * Returns: whether we're estimating
 */
gboolean
flatpak_transaction_progress_get_is_estimating (FlatpakTransactionProgress *self)
{
  return flatpak_progress_get_estimating (self->progress_obj);
}

/**
 * flatpak_transaction_progress_get_progress:
 * @self: a #FlatpakTransactionProgress
 *
 * Gets the current progress.
 *
 * Returns: the current progress, as an integer between 0 and 100
 */
int
flatpak_transaction_progress_get_progress (FlatpakTransactionProgress *self)
{
  return flatpak_progress_get_progress (self->progress_obj);
}

/**
 * flatpak_transaction_progress_get_bytes_transferred:
 * @self: a #FlatpakTransactionProgress
 *
 * Gets the number of bytes that have been transferred.
 *
 * Returns: the number of bytes transferred
 * Since: 1.1.2
 */
guint64
flatpak_transaction_progress_get_bytes_transferred (FlatpakTransactionProgress *self)
{
  guint64 bytes_transferred, transferred_extra_data_bytes;

  bytes_transferred = flatpak_progress_get_bytes_transferred (self->progress_obj);
  transferred_extra_data_bytes = flatpak_progress_get_transferred_extra_data_bytes (self->progress_obj);

  return bytes_transferred + transferred_extra_data_bytes;
}

/**
 * flatpak_transaction_progress_get_start_time:
 * @self: a #FlatpakTransactionProgress
 *
 * Gets the time at which this operation has started, as monotonic time.
 *
 * Returns: the start time
 * Since: 1.1.2
 */
guint64
flatpak_transaction_progress_get_start_time (FlatpakTransactionProgress *self)
{
  return flatpak_progress_get_start_time (self->progress_obj);
}

static void
flatpak_transaction_progress_finalize (GObject *object)
{
  FlatpakTransactionProgress *self = (FlatpakTransactionProgress *) object;

  g_object_unref (self->progress_obj);

  G_OBJECT_CLASS (flatpak_transaction_progress_parent_class)->finalize (object);
}

static void
flatpak_transaction_progress_class_init (FlatpakTransactionProgressClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = flatpak_transaction_progress_finalize;

  /**
   * FlatpakTransactionProgress::changed:
   * @object: A #FlatpakTransactionProgress
   *
   * Emitted when some detail of the progress object changes, you can call the various methods to get the current status.
   */
  progress_signals[CHANGED] =
    g_signal_new ("changed",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  0,
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE, 0);
}

static void
got_progress_cb (const char *status,
                 guint       progress,
                 gboolean    estimating,
                 gpointer    user_data)
{
  FlatpakTransactionProgress *p = user_data;

  if (!flatpak_progress_is_done (p->progress_obj))
    g_signal_emit (p, progress_signals[CHANGED], 0);
}

static void
flatpak_transaction_progress_init (FlatpakTransactionProgress *self)
{
  self->progress_obj = flatpak_progress_new (got_progress_cb, self);
}

static void
flatpak_transaction_progress_done (FlatpakTransactionProgress *self)
{
  flatpak_progress_done (self->progress_obj);
}

static FlatpakTransactionProgress *
flatpak_transaction_progress_new (void)
{
  return g_object_new (FLATPAK_TYPE_TRANSACTION_PROGRESS, NULL);
}

static guint signals[LAST_SIGNAL] = { 0 };

static void initable_iface_init (GInitableIface *initable_iface);

G_DEFINE_TYPE_WITH_CODE (FlatpakTransaction, flatpak_transaction, G_TYPE_OBJECT,
                         G_ADD_PRIVATE (FlatpakTransaction)
                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init))

static gboolean
transaction_is_local_only (FlatpakTransaction             *self,
                           FlatpakTransactionOperationType kind)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  return priv->no_pull || kind == FLATPAK_TRANSACTION_OPERATION_UNINSTALL;
}

static gboolean
remote_name_is_file (const char *remote_name)
{
  return remote_name != NULL &&
         g_str_has_prefix (remote_name, "file://");
}

/**
 * flatpak_transaction_add_dependency_source:
 * @self: a #FlatpakTransaction
 * @installation: a #FlatpakInstallation
 *
 * Adds an extra installation as a source for application dependencies.
 * This means that applications can be installed in this transaction relying
 * on runtimes from this additional installation (whereas it would normally
 * install required runtimes that are not installed in the installation
 * the transaction works on).
 *
 * Also see flatpak_transaction_add_default_dependency_sources().
 */
void
flatpak_transaction_add_dependency_source (FlatpakTransaction  *self,
                                           FlatpakInstallation *installation)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  g_ptr_array_add (priv->extra_dependency_dirs,
                   flatpak_installation_clone_dir_noensure (installation));
}

/**
 * flatpak_transaction_add_sideload_repo:
 * @self: a #FlatpakTransaction
 * @path: a path to a local flatpak repository
 *
 * Adds an extra local ostree repo as source for installation. This is
 * equivalent to using the sideload-repos directories (see flatpak(1)), but can
 * be done dynamically. Any path added here is used in addition to ones in
 * those directories.
 *
 * Since: 1.7.1
 */
void
flatpak_transaction_add_sideload_repo (FlatpakTransaction  *self,
                                       const char          *path)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  g_ptr_array_add (priv->extra_sideload_repos,
                   g_strdup (path));
}

/**
 * flatpak_transaction_add_default_dependency_sources:
 * @self: a #FlatpakTransaction
 *
 * Similar to flatpak_transaction_add_dependency_source(), but adds
 * all the default installations, which means all the defined system-wide
 * (but not per-user) installations.
 */
void
flatpak_transaction_add_default_dependency_sources (FlatpakTransaction *self)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(GPtrArray) system_dirs = NULL;
  GFile *path = flatpak_dir_get_path (priv->dir);
  int i;

  system_dirs = flatpak_dir_get_system_list (NULL, NULL);
  if (system_dirs == NULL)
    return;

  for (i = 0; i < system_dirs->len; i++)
    {
      FlatpakDir *system_dir = g_ptr_array_index (system_dirs, i);
      GFile *system_path = flatpak_dir_get_path (system_dir);

      if (g_file_equal (path, system_path))
        continue;

      g_ptr_array_add (priv->extra_dependency_dirs, g_object_ref (system_dir));
    }
}

/* Check if the ref is in the dir, or in the extra dependency source dir, in case its a
 * user-dir or another system-wide installation. We want to avoid depending
 * on user-installed things when installing to the system dir.
 */
static gboolean
ref_is_installed (FlatpakTransaction *self,
                  FlatpakDecomposed *ref)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(GFile) deploy_dir = NULL;
  FlatpakDir *dir = priv->dir;
  int i;

  deploy_dir = flatpak_dir_get_if_deployed (dir, ref, NULL, NULL);
  if (deploy_dir != NULL)
    return TRUE;

  for (i = 0; i < priv->extra_dependency_dirs->len; i++)
    {
      FlatpakDir *dependency_dir = g_ptr_array_index (priv->extra_dependency_dirs, i);

      deploy_dir = flatpak_dir_get_if_deployed (dependency_dir, ref, NULL, NULL);
      if (deploy_dir != NULL)
        return TRUE;
    }

  return FALSE;
}

static gboolean
dir_ref_is_installed (FlatpakDir *dir, FlatpakDecomposed *ref, char **remote_out, GBytes **deploy_data_out)
{
  g_autoptr(GBytes) deploy_data = NULL;

  deploy_data = flatpak_dir_get_deploy_data (dir, ref, FLATPAK_DEPLOY_VERSION_ANY, NULL, NULL);
  if (deploy_data == NULL)
    return FALSE;

  if (remote_out)
    *remote_out = g_strdup (flatpak_deploy_data_get_origin (deploy_data));

  if (deploy_data_out)
    *deploy_data_out = g_bytes_ref (deploy_data);

  return TRUE;
}

/**
 * SECTION:flatpak-transaction-operation
 * @Title: FlatpakTransactionOperation
 * @Short_description: Operation in a transaction
 *
 * FlatpakTransactionOperation is an object that represents a single operation
 * in a transaction. You receive a FlatpakTransactionOperation object with the
 * #FlatpakTransaction::new-operation signal.
 */

G_DEFINE_TYPE (FlatpakTransactionOperation, flatpak_transaction_operation, G_TYPE_OBJECT)

static void
flatpak_transaction_operation_finalize (GObject *object)
{
  FlatpakTransactionOperation *self = (FlatpakTransactionOperation *) object;

  g_free (self->remote);
  flatpak_decomposed_unref (self->ref);
  g_free (self->commit);
  g_strfreev (self->subpaths);
  g_clear_object (&self->bundle);
  g_free (self->eol);
  g_free (self->eol_rebase);
  if (self->previous_ids)
    g_strfreev (self->previous_ids);
  if (self->external_metadata)
    g_bytes_unref (self->external_metadata);
  g_free (self->resolved_commit);
  if (self->resolved_sideload_path)
    g_object_unref (self->resolved_sideload_path);
  if (self->resolved_metadata)
    g_bytes_unref (self->resolved_metadata);
  if (self->resolved_metakey)
    g_key_file_unref (self->resolved_metakey);
  if (self->resolved_old_metadata)
    g_bytes_unref (self->resolved_old_metadata);
  if (self->resolved_old_metakey)
    g_key_file_unref (self->resolved_old_metakey);
  g_free (self->resolved_token);
  g_list_free (self->run_before_ops);
  if (self->related_to_ops)
    g_ptr_array_unref (self->related_to_ops);
  if (self->summary_metadata)
    g_variant_unref (self->summary_metadata);

  G_OBJECT_CLASS (flatpak_transaction_operation_parent_class)->finalize (object);
}

static void
flatpak_transaction_operation_class_init (FlatpakTransactionOperationClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = flatpak_transaction_operation_finalize;
}

static void
flatpak_transaction_operation_init (FlatpakTransactionOperation *self)
{
}

static FlatpakTransactionOperation *
flatpak_transaction_operation_new (const char                     *remote,
                                   FlatpakDecomposed              *ref,
                                   const char                    **subpaths,
                                   const char                    **previous_ids,
                                   const char                     *commit,
                                   GFile                          *bundle,
                                   FlatpakTransactionOperationType kind,
                                   gboolean                        pin_on_deploy)
{
  FlatpakTransactionOperation *self;

  self = g_object_new (FLATPAK_TYPE_TRANSACTION_OPERATION, NULL);

  self->remote = g_strdup (remote);
  self->ref = flatpak_decomposed_ref (ref);
  self->subpaths = g_strdupv ((char **) subpaths);
  self->previous_ids = g_strdupv ((char **) previous_ids);
  self->commit = g_strdup (commit);
  if (bundle)
    self->bundle = g_object_ref (bundle);
  self->kind = kind;
  self->pin_on_deploy = pin_on_deploy;

  return self;
}

/**
 * flatpak_transaction_operation_get_operation_type:
 * @self: a #FlatpakTransactionOperation
 *
 * Gets the type of the operation.
 *
 * Returns: the type of operation, as #FlatpakTransactionOperationType
 */
FlatpakTransactionOperationType
flatpak_transaction_operation_get_operation_type (FlatpakTransactionOperation *self)
{
  return self->kind;
}

/**
 * flatpak_transaction_operation_get_ref:
 * @self: a #FlatpakTransactionOperation
 *
 * Gets the ref that the operation applies to.
 *
 * Returns: (transfer none): the ref
 */
const char *
flatpak_transaction_operation_get_ref (FlatpakTransactionOperation *self)
{
  return flatpak_decomposed_get_ref (self->ref);
}

FlatpakDecomposed *
flatpak_transaction_operation_get_decomposed (FlatpakTransactionOperation *self)
{
  return self->ref;
}

/**
 * flatpak_transaction_operation_get_related_to_ops:
 * @self: a #FlatpakTransactionOperation
 *
 * Gets the operation(s) which caused this operation to be added to the
 * transaction. In the case of a runtime, it's the app(s) whose runtime it is,
 * and/or a runtime extension in the special case of an extra-data extension
 * that doesn't define the "NoRuntime" key. In the case of a related ref such
 * as an extension, it's the main app or runtime. In the case of a main app or
 * something added to the transaction by e.g. flatpak_transaction_add_install()
 * and which is not otherwise needed, %NULL or an empty array will be returned.
 *
 * Note that an op will be returned even if it’s marked as to be skipped when
 * the transaction is run. Check that using
 * flatpak_transaction_operation_get_is_skipped().
 *
 * Elements in the returned array are only safe to access while the parent
 * #FlatpakTransaction is alive.
 *
 * Returns: (transfer none) (element-type FlatpakTransactionOperation) (nullable): the
 *   #FlatpakTransactionOperations this one is related to (may be %NULL or an
 *   empty array, which are equivalent)
 * Since: 1.7.3
 */
GPtrArray *
flatpak_transaction_operation_get_related_to_ops (FlatpakTransactionOperation *self)
{
  return self->related_to_ops;
}

static void
flatpak_transaction_operation_add_related_to_op (FlatpakTransactionOperation *op,
                                                 FlatpakTransactionOperation *related_op)
{
  if (op->related_to_ops == NULL)
    op->related_to_ops = g_ptr_array_new ();
  g_ptr_array_add (op->related_to_ops, related_op);
}

/**
 * flatpak_transaction_operation_get_is_skipped:
 * @self: a #FlatpakTransactionOperation
 *
 * Gets whether this operation will be skipped when the transaction is run.
 * Operations are skipped in some transaction situations, for example when an
 * app has reached end of life and needs a rebase, or when it would have been
 * updated but no update is available. By default, skipped
 * operations are not returned by flatpak_transaction_get_operations() — but
 * they can be accessed by traversing the operation graph using
 * flatpak_transaction_operation_get_related_to_ops().
 *
 * Returns: %TRUE if the operation has been marked as to skip, %FALSE otherwise
 * Since: 1.7.3
 */
gboolean
flatpak_transaction_operation_get_is_skipped (FlatpakTransactionOperation *self)
{
  return self->skip;
}

/**
 * flatpak_transaction_operation_get_remote:
 * @self: a #FlatpakTransactionOperation
 *
 * Gets the remote that the operation applies to.
 *
 * Returns: (transfer none): the remote
 */
const char *
flatpak_transaction_operation_get_remote (FlatpakTransactionOperation *self)
{
  return self->remote;
}

/**
 * flatpak_transaction_operation_type_to_string:
 * @kind: a #FlatpakTransactionOperationType
 *
 * Converts the operation type to a string.
 *
 * Returns: (transfer none): a string representing @kind
 */
const char *
flatpak_transaction_operation_type_to_string (FlatpakTransactionOperationType kind)
{
  if (kind == FLATPAK_TRANSACTION_OPERATION_INSTALL)
    return "install";
  if (kind == FLATPAK_TRANSACTION_OPERATION_UPDATE)
    return "update";
  if (kind == FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE)
    return "install-bundle";
  if (kind == FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
    return "uninstall";
  return NULL;
}

/**
 * flatpak_transaction_operation_get_bundle_path:
 * @self: a #FlatpakTransactionOperation
 *
 * Gets the path to the bundle.
 *
 * Returns: (transfer none): the bundle #GFile or %NULL
 */
GFile *
flatpak_transaction_operation_get_bundle_path (FlatpakTransactionOperation *self)
{
  return self->bundle;
}

/**
 * flatpak_transaction_operation_get_commit:
 * @self: a #FlatpakTransactionOperation
 *
 * Gets the commit ID for the operation.
 *
 * This information is available when the transaction is resolved,
 * i.e. when #FlatpakTransaction::ready is emitted.
 *
 * Returns: (transfer none): the commit ID
 */
const char *
flatpak_transaction_operation_get_commit (FlatpakTransactionOperation *self)
{
  return self->resolved_commit;
}

/**
 * flatpak_transaction_operation_get_download_size:
 * @self: a #flatpakTransactionOperation
 *
 * Gets the maximum download size for the operation.
 *
 * Note that this does not include the size of dependencies, and
 * the actual download may be smaller, if some of the data is already
 * available locally.
 *
 * For uninstall operations, this returns 0.
 *
 * This information is available when the transaction is resolved,
 * i.e. when #FlatpakTransaction::ready is emitted.
 *
 * Returns: the download size, in bytes
 * Since: 1.1.2
 */
guint64
flatpak_transaction_operation_get_download_size (FlatpakTransactionOperation *self)
{
  return self->download_size;
}

/**
 * flatpak_transaction_operation_get_installed_size:
 * @self: a #flatpakTransactionOperation
 *
 * Gets the installed size for the operation.
 *
 * Note that even for a new install, the extra space required on
 * disk may be smaller than this number, if some of the data is already
 * available locally.
 *
 * For uninstall operations, this returns 0.
 *
 * This information is available when the transaction is resolved,
 * i.e. when #FlatpakTransaction::ready is emitted.
 *
 * Returns: the installed size, in bytes
 * Since: 1.1.2
 */
guint64
flatpak_transaction_operation_get_installed_size (FlatpakTransactionOperation *self)
{
  return self->installed_size;
}

/**
 * flatpak_transaction_operation_get_metadata:
 * @self: a #FlatpakTransactionOperation
 *
 * Gets the metadata that will be applicable when the
 * operation is done.
 *
 * This can be compared to the current metadata returned
 * by flatpak_transaction_operation_get_old_metadata()
 * to find new required permissions and similar changes.
 *
 * This information is available when the transaction is resolved,
 * i.e. when #FlatpakTransaction::ready is emitted.
 *
 * Returns: (transfer none): the metadata #GKeyFile
 */
GKeyFile *
flatpak_transaction_operation_get_metadata (FlatpakTransactionOperation *self)
{
  return self->resolved_metakey;
}

/**
 * flatpak_transaction_operation_get_old_metadata:
 * @self: a #FlatpakTransactionOperation
 *
 * Gets the metadata current metadata for the ref that @self works on.
 * Also see flatpak_transaction_operation_get_metadata().
 *
 * This information is available when the transaction is resolved,
 * i.e. when #FlatpakTransaction::ready is emitted.
 *
 * Returns: (transfer none): the old metadata #GKeyFile
 */
GKeyFile *
flatpak_transaction_operation_get_old_metadata (FlatpakTransactionOperation *self)
{
  return self->resolved_old_metakey;
}

/**
 * flatpak_transaction_operation_get_subpaths:
 * @self: a #FlatpakTransactionOperation
 *
 * Gets the set of subpaths that will be pulled from this ref.
 *
 * Some refs are only partially installed, such as translations. These
 * are subset by the toplevel directory (typically by translation name).
 * The subset to install can be specified at install time, but is otherwise
 * decided based on configurations and things like the current locale and
 * how the app was previously installed.
 *
 * If there is no subsetting active, this will always return %NULL
 * (even though some other APIs also take an empty string to mean no
 * subsetting).
 *
 * This information is available when the transaction is resolved,
 * i.e. when #FlatpakTransaction::ready is emitted.
 *
 * Returns: (transfer none): the set of subpaths that will be pulled, or %NULL if no subsetting.
 * Since: 1.9.1
 */
const char * const *
flatpak_transaction_operation_get_subpaths (FlatpakTransactionOperation *self)
{
  if (self->subpaths == NULL || self->subpaths[0] == NULL)
    return NULL;

  return (const char * const *) self->subpaths;
}


/**
 * flatpak_transaction_operation_get_requires_authentication:
 * @self: a #FlatpakTransactionOperation
 *
 * Gets whether the given operation will require authentication to acquire
 * needed tokens. See also the documentation for
 * #FlatpakTransaction::ready-pre-auth.
 *
 * Returns: whether @self requires authentication
 * Since: 1.9.1
 */
gboolean
flatpak_transaction_operation_get_requires_authentication (FlatpakTransactionOperation *self)
{
  return
    op_may_need_token (self) &&
    self->token_type != 0 &&
    !self->requested_token;
}

/**
 * flatpak_transaction_is_empty:
 * @self: a #FlatpakTransaction
 *
 * Returns whether the transaction contains any non-skipped operations.
 *
 * Returns: %TRUE if the transaction is empty
 */
gboolean
flatpak_transaction_is_empty (FlatpakTransaction *self)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  GList *l;

  for (l = priv->ops; l; l = l->next)
    {
      FlatpakTransactionOperation *op = l->data;

      if (!op->skip)
        return FALSE;
    }

  return TRUE;
}

static void
flatpak_transaction_finalize (GObject *object)
{
  FlatpakTransaction *self = (FlatpakTransaction *) object;
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  g_clear_object (&priv->installation);

  g_free (priv->parent_window);
  g_list_free_full (priv->flatpakrefs, (GDestroyNotify) g_key_file_unref);
  g_list_free_full (priv->bundles, (GDestroyNotify) bundle_data_free);
  g_free (priv->default_arch);
  g_hash_table_unref (priv->last_op_for_ref);
  g_hash_table_unref (priv->remote_states);
  g_list_free_full (priv->ops, (GDestroyNotify) g_object_unref);
  g_clear_object (&priv->dir);

  g_ptr_array_unref (priv->added_origin_remotes);

  g_ptr_array_free (priv->extra_dependency_dirs, TRUE);
  g_ptr_array_free (priv->extra_sideload_repos, TRUE);

  G_OBJECT_CLASS (flatpak_transaction_parent_class)->finalize (object);
}

static void
flatpak_transaction_set_property (GObject      *object,
                                  guint         prop_id,
                                  const GValue *value,
                                  GParamSpec   *pspec)
{
  FlatpakTransaction *self = FLATPAK_TRANSACTION (object);
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  switch ((FlatpakTransactionProperty) prop_id)
    {
    case PROP_INSTALLATION:
      g_clear_object (&priv->installation);
      priv->installation = g_value_dup_object (value);
      break;

    case PROP_NO_INTERACTION:
      flatpak_transaction_set_no_interaction (self, g_value_get_boolean (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static gboolean
signal_accumulator_false_abort (GSignalInvocationHint *ihint,
                                GValue                *return_accu,
                                const GValue          *handler_return,
                                gpointer               dummy)
{
  gboolean continue_emission;
  gboolean signal_continue;

  signal_continue = g_value_get_boolean (handler_return);
  g_value_set_boolean (return_accu, signal_continue);
  continue_emission = signal_continue;

  return continue_emission;
}

static void
flatpak_transaction_get_property (GObject    *object,
                                  guint       prop_id,
                                  GValue     *value,
                                  GParamSpec *pspec)
{
  FlatpakTransaction *self = FLATPAK_TRANSACTION (object);
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  switch ((FlatpakTransactionProperty) prop_id)
    {
    case PROP_INSTALLATION:
      g_value_set_object (value, priv->installation);
      break;

    case PROP_NO_INTERACTION:
      g_value_set_boolean (value, flatpak_transaction_get_no_interaction (self));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static gboolean
flatpak_transaction_ready (FlatpakTransaction *transaction)
{
  return TRUE;
}

static gboolean
flatpak_transaction_ready_pre_auth (FlatpakTransaction *transaction)
{
  return TRUE;
}

static gboolean
flatpak_transaction_add_new_remote (FlatpakTransaction            *transaction,
                                    FlatpakTransactionRemoteReason reason,
                                    const char                    *from_id,
                                    const char                    *suggested_remote_name,
                                    const char                    *url)
{
  return FALSE;
}

static void
flatpak_transaction_install_authenticator  (FlatpakTransaction *transaction,
                                            const char         *remote,
                                            const char         *authenticator_ref)
{
}

static gboolean flatpak_transaction_real_run (FlatpakTransaction *transaction,
                                              GCancellable       *cancellable,
                                              GError            **error);

static void
flatpak_transaction_class_init (FlatpakTransactionClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  klass->ready = flatpak_transaction_ready;
  klass->ready_pre_auth = flatpak_transaction_ready_pre_auth;
  klass->add_new_remote = flatpak_transaction_add_new_remote;
  klass->install_authenticator = flatpak_transaction_install_authenticator;
  klass->run = flatpak_transaction_real_run;
  object_class->finalize = flatpak_transaction_finalize;
  object_class->get_property = flatpak_transaction_get_property;
  object_class->set_property = flatpak_transaction_set_property;

  /**
   * FlatpakTransaction:installation:
   *
   * The installation that the transaction operates on.
   */
  g_object_class_install_property (object_class,
                                   PROP_INSTALLATION,
                                   g_param_spec_object ("installation",
                                                        "Installation",
                                                        "The installation instance",
                                                        FLATPAK_TYPE_INSTALLATION,
                                                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  /**
   * FlatpakTransaction:no-interaction:
   *
   * %TRUE if the transaction is not interactive, %FALSE otherwise.
   *
   * See flatpak_transaction_set_no_interaction().
   *
   * Since: 1.13.0
   */
  g_object_class_install_property (object_class,
                                   PROP_NO_INTERACTION,
                                   g_param_spec_boolean ("no-interaction",
                                                         "No Interaction",
                                                         "The installation instance",
                                                         FALSE,
                                                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY));

  /**
   * FlatpakTransaction::new-operation:
   * @object: A #FlatpakTransaction
   * @operation: The new #FlatpakTransactionOperation
   * @progress: A #FlatpakTransactionProgress for @operation
   *
   * The ::new-operation signal gets emitted during the execution of
   * the transaction when a new operation is beginning.
   */
  signals[NEW_OPERATION] =
    g_signal_new ("new-operation",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (FlatpakTransactionClass, new_operation),
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE, 2, FLATPAK_TYPE_TRANSACTION_OPERATION, FLATPAK_TYPE_TRANSACTION_PROGRESS);

  /**
   * FlatpakTransaction::operation-error:
   * @object: A #FlatpakTransaction
   * @operation: The #FlatpakTransactionOperation which failed
   * @error: A #GError
   * @details: (type FlatpakTransactionErrorDetails): A #FlatpakTransactionErrorDetails with details about the error
   *
   * The ::operation-error signal gets emitted when an error occurs during the
   * execution of the transaction.
   *
   * Returns: the %TRUE to continue transaction, %FALSE to stop
   */
  signals[OPERATION_ERROR] =
    g_signal_new ("operation-error",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (FlatpakTransactionClass, operation_error),
                  NULL, NULL,
                  NULL,
                  G_TYPE_BOOLEAN, 3, FLATPAK_TYPE_TRANSACTION_OPERATION, G_TYPE_ERROR, G_TYPE_INT);

  /**
   * FlatpakTransaction::operation-done:
   * @object: A #FlatpakTransaction
   * @operation: The #FlatpakTransactionOperation which finished
   * @commit: (nullable): The commit
   * @result: (type FlatpakTransactionResult): A #FlatpakTransactionResult giving details about the result
   *
   * The ::operation-done signal gets emitted during the execution of
   * the transaction when an operation is finished.
   */
  signals[OPERATION_DONE] =
    g_signal_new ("operation-done",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (FlatpakTransactionClass, operation_done),
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE, 3, FLATPAK_TYPE_TRANSACTION_OPERATION, G_TYPE_STRING, G_TYPE_INT);

  /**
   * FlatpakTransaction::choose-remote-for-ref:
   * @object: A #FlatpakTransaction
   * @for_ref: The ref we are installing
   * @runtime_ref: The ref we are looking for
   * @remotes: the remotes that has the ref, sorted in prio order
   *
   * The ::choose-remote-for-ref signal gets emitted when a
   * remote needs to be selected during the execution of the transaction.
   *
   * Returns: the index of the remote to use, or -1 to not pick one (and fail)
   */
  signals[CHOOSE_REMOTE_FOR_REF] =
    g_signal_new ("choose-remote-for-ref",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (FlatpakTransactionClass, choose_remote_for_ref),
                  NULL, NULL,
                  NULL,
                  G_TYPE_INT, 3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRV);

  /**
   * FlatpakTransaction::end-of-lifed:
   * @object: A #FlatpakTransaction
   * @ref: The ref we are installing
   * @reason: The eol reason, or %NULL
   * @rebase: The new name, if rebased, or %NULL
   *
   * The ::end-of-lifed signal gets emitted when a ref is found to
   * be marked as end-of-life during the execution of the transaction.
   */
  signals[END_OF_LIFED] =
    g_signal_new ("end-of-lifed",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (FlatpakTransactionClass, end_of_lifed),
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE, 3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);

  /**
   * FlatpakTransaction::end-of-lifed-with-rebase:
   * @object: A #FlatpakTransaction
   * @remote: The remote for the ref we are processing
   * @ref: The ref we are processing
   * @reason: The eol reason, or %NULL
   * @rebased_to_ref: The new name, if rebased, or %NULL
   * @previous_ids: The previous names for the rebased ref (if any), including the one from @ref
   *
   * The ::end-of-lifed-with-rebase signal gets emitted when a ref is found
   * to be marked as end-of-life before the transaction begins. Unlike
   * #FlatpakTransaction::end-of-lifed, this signal allows for the
   * transaction to be modified in order to e.g. install the rebased
   * ref.
   *
   * If the caller wants to install the rebased ref, they should call
   * flatpak_transaction_add_rebase_and_uninstall() on @rebased_to_ref and @ref,
   * and return %TRUE. Otherwise %FALSE may be returned.
   *
   * Returns: %TRUE if the operation on this end-of-lifed ref should
   * be skipped (e.g. because the rebased ref has been added to the
   * transaction), %FALSE if it should remain.
   *
   * Since: 1.3.2
   */
  signals[END_OF_LIFED_WITH_REBASE] =
    g_signal_new ("end-of-lifed-with-rebase",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (FlatpakTransactionClass, end_of_lifed_with_rebase),
                  NULL, NULL,
                  NULL,
                  G_TYPE_BOOLEAN, 5, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRV);

  /**
   * FlatpakTransaction::ready:
   * @object: A #FlatpakTransaction
   *
   * The ::ready signal is emitted when all the refs involved in the operation
   * have been resolved to commits, and the required authentication for all ops is gotten.
   * At this point flatpak_transaction_get_operations() will return all the operations
   * that will be executed as part of the transaction.
   *
   * Returns: %TRUE to carry on with the transaction, %FALSE to abort
   */
  signals[READY] =
    g_signal_new ("ready",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (FlatpakTransactionClass, ready),
                  signal_accumulator_false_abort, NULL,
                  NULL,
                  G_TYPE_BOOLEAN, 0);

  /**
   * FlatpakTransaction::ready-pre-auth:
   * @object: A #FlatpakTransaction
   *
   * The ::ready-pre-auth signal is emitted when all the refs involved in the
   * transaction have been resolved to commits, but we might not necessarily
   * have asked for authentication for all their required operations. This is
   * very similar to the ::ready signal, and you can choose which one (or both)
   * to use depending on how you want to handle authentication in your user
   * interface.
   *
   * At this point flatpak_transaction_get_operations() will return all the
   * operations that will be executed as part of the transaction. You can call
   * flatpak_transaction_operation_get_requires_authentication() to see which
   * will require authentication.
   *
   * Returns: %TRUE to carry on with the transaction, %FALSE to abort
   *
   * Since: 1.9.1
   */
  signals[READY_PRE_AUTH] =
    g_signal_new ("ready-pre-auth",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (FlatpakTransactionClass, ready_pre_auth),
                  signal_accumulator_false_abort, NULL,
                  NULL,
                  G_TYPE_BOOLEAN, 0);

  /**
   * FlatpakTransaction::add-new-remote:
   * @object: A #FlatpakTransaction
   * @reason: (type FlatpakTransactionRemoteReason): A #FlatpakTransactionRemoteReason for this suggestion
   * @from_id: The id of the app/runtime
   * @suggested_remote_name: The suggested remote name
   * @url: The repo url
   *
   * The ::add-new-remote signal gets emitted if, as part of the transaction,
   * it is required or recommended that a new remote is added, for the reason
   * described in @reason.
   *
   * Returns: %TRUE to add the remote
   */
  signals[ADD_NEW_REMOTE] =
    g_signal_new ("add-new-remote",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (FlatpakTransactionClass, add_new_remote),
                  g_signal_accumulator_first_wins, NULL,
                  NULL,
                  G_TYPE_BOOLEAN, 4, G_TYPE_INT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);

  /**
   * FlatpakTransaction::install-authenticator:
   * @object: A #FlatpakTransaction
   * @remote: The remote name
   * @authenticator_ref: The ref for the authenticator
   *
   * The ::install-authenticator signal gets emitted if, as part of
   * resolving the transaction, we need to use an authenticator, but the authentication
   * is not installed, but is available to be installed from the ref.
   *
   * The application can handle this signal, and if so create another transaction
   * to install the authenticator.
   *
   * The default handler does nothing, and if the authenticator is not installed when
   * the signal handler fails the transaction will error out.
   *
   * Since: 1.8.0
   */
  signals[INSTALL_AUTHENTICATOR] =
    g_signal_new ("install-authenticator",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (FlatpakTransactionClass, install_authenticator),
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING);

  /**
   * FlatpakTransaction::webflow-start:
   * @object: A #FlatpakTransaction
   * @remote: The remote we're authenticating with
   * @url: The url to show
   * @options: Extra options, currently unused
   * @id: The id of the operation, can be used to cancel it
   *
   * The ::webflow-start signal gets emitted when some kind of user
   * authentication is needed during the operation. If the caller handles this
   * it should show the url in a webbrowser and return %TRUE. This will
   * eventually cause the webbrowser to finish the authentication operation and
   * operation will continue, as signaled by the webflow-done being emitted.
   *
   * If the client does not support webflow then return %FALSE from this signal
   * (or don't implement it). This will abort the authentication and likely
   * result in the transaction failing (unless the authentication was somehow
   * optional).
   *
   * During the time between webflow-start and webflow-done the client can call
   * flatpak_transaction_abort_webflow() to manually abort the authentication.
   * This is useful if the user aborted the authentication operation some way,
   * like e.g. closing the browser window.
   *
   * Since: 1.5.1
   */
  signals[WEBFLOW_START] =
    g_signal_new ("webflow-start",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (FlatpakTransactionClass, webflow_start),
                  NULL, NULL,
                  NULL,
                  G_TYPE_BOOLEAN, 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_VARIANT, G_TYPE_INT);
  /**
   * FlatpakTransaction::webflow-done:
   * @object: A #FlatpakTransaction
   * @options: Extra options, currently unused
   * @id: The id of the operation
   *
   * The ::webflow-done signal gets emitted when the authentication
   * finished the webflow, independent of the reason and results.  If
   * you for were showing a web-browser window it can now be closed.
   *
   * Since: 1.5.1
   */
  signals[WEBFLOW_DONE] =
    g_signal_new ("webflow-done",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (FlatpakTransactionClass, webflow_done),
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE, 2, G_TYPE_VARIANT, G_TYPE_INT);
  /**
   * FlatpakTransaction::basic-auth-start:
   * @object: A #FlatpakTransaction
   * @remote: The remote we're authenticating with
   * @realm: The url to show
   * @options: Extra options, currently unused
   * @id: The id of the operation, can be used to finish it
   *
   * The ::basic-auth-start signal gets emitted when a basic user/password
   * authentication is needed during the operation. If the caller handles this
   * it should ask the user for the user and password and return %TRUE. Once
   * the information is gathered call flatpak_transaction_complete_basic_auth()
   * with it.
   *
   * If the client does not support basic auth then return %FALSE from this signal
   * (or don't implement it). This will abort the authentication and likely
   * result in the transaction failing (unless the authentication was somehow
   * optional).
   *
   * Since: 1.5.2
   */
  signals[BASIC_AUTH_START] =
    g_signal_new ("basic-auth-start",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (FlatpakTransactionClass, basic_auth_start),
                  NULL, NULL,
                  NULL,
                  G_TYPE_BOOLEAN, 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_VARIANT, G_TYPE_INT);

}

static void
flatpak_transaction_init (FlatpakTransaction *self)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  priv->last_op_for_ref = g_hash_table_new_full ((GHashFunc)flatpak_decomposed_hash, (GEqualFunc)flatpak_decomposed_equal, (GDestroyNotify) flatpak_decomposed_unref, NULL);
  priv->remote_states = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify) flatpak_remote_state_unref);
  priv->added_origin_remotes = g_ptr_array_new_with_free_func (g_free);
  priv->extra_dependency_dirs = g_ptr_array_new_with_free_func (g_object_unref);
  priv->extra_sideload_repos = g_ptr_array_new_with_free_func (g_free);
  priv->can_run = TRUE;
}


static gboolean
initable_init (GInitable    *initable,
               GCancellable *cancellable,
               GError      **error)
{
  FlatpakTransaction *self = FLATPAK_TRANSACTION (initable);
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(FlatpakDir) dir = NULL;

  if (priv->installation == NULL)
    return flatpak_fail (error, "No installation specified");

  dir = flatpak_installation_clone_dir (priv->installation, cancellable, error);
  if (dir == NULL)
    return FALSE;

  priv->dir = g_steal_pointer (&dir);

  return TRUE;
}

static void
initable_iface_init (GInitableIface *initable_iface)
{
  initable_iface->init = initable_init;
}

/**
 * flatpak_transaction_new_for_installation:
 * @installation: a #FlatpakInstallation
 * @cancellable: (nullable): a #GCancellable
 * @error: return location for a #GError
 *
 * Creates a new #FlatpakTransaction object that can be used to do installation
 * and updates of multiple refs, as well as their dependencies, in a single
 * operation. Set the options you want on the transaction and add the
 * refs you want to install/update, then start the transaction with
 * flatpak_transaction_run ().
 *
 * Returns: (transfer full): a #FlatpakTransaction, or %NULL on failure.
 */
FlatpakTransaction *
flatpak_transaction_new_for_installation (FlatpakInstallation *installation,
                                          GCancellable        *cancellable,
                                          GError             **error)
{
  return g_initable_new (FLATPAK_TYPE_TRANSACTION,
                         cancellable, error,
                         "installation", installation,
                         NULL);
}

/**
 * flatpak_transaction_set_no_pull:
 * @self: a #FlatpakTransaction
 * @no_pull: whether to avoid pulls
 *
 * Sets whether the transaction should operate only on locally
 * available data.
 */
void
flatpak_transaction_set_no_pull (FlatpakTransaction *self,
                                 gboolean            no_pull)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  priv->no_pull = no_pull;
}

/**
 * flatpak_transaction_get_no_pull:
 * @self: a #FlatpakTransaction
 *
 * Gets whether the transaction should operate only on locally
 * available data.
 *
 * Returns: %TRUE if no_pull is set, %FALSE otherwise
 *
 * Since: 1.5.1
 */
gboolean
flatpak_transaction_get_no_pull (FlatpakTransaction *self)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  return priv->no_pull;
}

/**
 * flatpak_transaction_set_parent_window:
 * @self: a #FlatpakTransaction
 * @parent_window: whether to avoid pulls
 *
 * Sets the parent window (if any) to use for any UI show by this transaction.
 * This is used by authenticators if they need to interact with the user during
 * authentication.
 *
 * The format of this string depends on the display system in use, and is the
 * same as used by xdg-desktop-portal.
 *
 * On X11 it should be of the form x11:$xid where $xid is the hex
 * version of the xwindows id.
 *
 * On wayland is should be wayland:$handle where handle is gotten by
 * using the export call of the xdg-foreign-unstable wayland extension.
 *
 * Since: 1.5.1
 */
void
flatpak_transaction_set_parent_window (FlatpakTransaction *self,
                                       const char *parent_window)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  g_free (priv->parent_window);
  priv->parent_window = g_strdup (parent_window);
}

/**
 * flatpak_transaction_get_parent_window:
 * @self: a #FlatpakTransaction
 *
 * Gets the parent window set for this transaction, or %NULL if unset. See
 * flatpak_transaction_get_parent_window().
 *
 * Returns: (transfer none): a window name, or %NULL
 *
 * Since: 1.5.1
 */
const char *
flatpak_transaction_get_parent_window (FlatpakTransaction *self)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  return priv->parent_window;
}

/**
 * flatpak_transaction_set_no_deploy:
 * @self: a #FlatpakTransaction
 * @no_deploy: whether to avoid deploying
 *
 * Sets whether the transaction should download updates, but
 * not deploy them.
 */
void
flatpak_transaction_set_no_deploy (FlatpakTransaction *self,
                                   gboolean            no_deploy)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  priv->no_deploy = no_deploy;
}

/**
 * flatpak_transaction_get_no_deploy:
 * @self: a #FlatpakTransaction
 *
 * Gets whether the transaction is only downloading updates,
 * and not deploying them.
 *
 * Returns: %TRUE if no_deploy is set, %FALSE otherwise
 *
 * Since: 1.5.1
 */
gboolean
flatpak_transaction_get_no_deploy (FlatpakTransaction *self)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  return priv->no_deploy;
}

/**
 * flatpak_transaction_set_disable_static_deltas:
 * @self: a #FlatpakTransaction
 * @disable_static_deltas: whether to avoid static deltas
 *
 * Sets whether the transaction should avoid using static
 * deltas when pulling.
 */
void
flatpak_transaction_set_disable_static_deltas (FlatpakTransaction *self,
                                               gboolean            disable_static_deltas)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  priv->disable_static_deltas = disable_static_deltas;
}

/**
 * flatpak_transaction_set_disable_prune:
 * @self: a #FlatpakTransaction
 * @disable_prune: whether to avoid pruning
 *
 * Sets whether the transaction should avoid pruning the local OSTree
 * repository after updating.
 */
void
flatpak_transaction_set_disable_prune (FlatpakTransaction *self,
                                       gboolean            disable_prune)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  priv->disable_prune = disable_prune;
}

/**
 * flatpak_transaction_set_disable_auto_pin:
 * @self: a #FlatpakTransaction
 * @disable_pin: whether to disable auto-pinning
 *
 * Normally the transaction pins any explicit installations so they will not
 * be automatically removed. But this can be disabled if you don't want this
 * behaviour.
 *
 * Since: 1.9.1
 */
void
flatpak_transaction_set_disable_auto_pin  (FlatpakTransaction *self,
                                           gboolean            disable_pin)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  priv->disable_auto_pin = disable_pin;
}

/**
 * flatpak_transaction_set_disable_dependencies:
 * @self: a #FlatpakTransaction
 * @disable_dependencies: whether to disable runtime dependencies
 *
 * Sets whether the transaction should ignore runtime dependencies
 * when resolving operations for applications.
 */
void
flatpak_transaction_set_disable_dependencies (FlatpakTransaction *self,
                                              gboolean            disable_dependencies)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  priv->disable_deps = disable_dependencies;
}

/**
 * flatpak_transaction_set_disable_related:
 * @self: a #FlatpakTransaction
 * @disable_related: whether to avoid adding related refs
 *
 * Sets whether the transaction should avoid adding related refs
 * when resolving operations. Related refs are extensions that are
 * suggested by apps, such as locales.
 */
void
flatpak_transaction_set_disable_related (FlatpakTransaction *self,
                                         gboolean            disable_related)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  priv->disable_related = disable_related;
}

/**
 * flatpak_transaction_set_reinstall:
 * @self: a #FlatpakTransaction
 * @reinstall: whether to reinstall refs
 *
 * Sets whether the transaction should uninstall first if a
 * ref is already installed.
 */
void
flatpak_transaction_set_reinstall (FlatpakTransaction *self,
                                   gboolean            reinstall)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  priv->reinstall = reinstall;
}

/**
 * flatpak_transaction_get_no_interaction:
 * @self: a #FlatpakTransaction
 *
 * Gets whether the transaction is interactive. See
 * flatpak_transaction_set_no_interaction().
 *
 * Returns: %TRUE if the transaction is not interactive, %FALSE otherwise
 * Since: 1.13.0
 */
gboolean
flatpak_transaction_get_no_interaction (FlatpakTransaction *self)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  return flatpak_dir_get_no_interaction (priv->dir);
}

/**
 * flatpak_transaction_set_no_interaction:
 * @self: a #FlatpakTransaction
 * @no_interaction: Whether to disallow interactive authorization for operations
 *
 * This method can be used to prevent interactive authorization dialogs to appear
 * for operations on @self. This is useful for background operations that are not
 * directly triggered by a user action.
 *
 * By default, the setting from the parent #FlatpakInstallation is used.
 *
 * Since: 1.7.3
 */
void
flatpak_transaction_set_no_interaction (FlatpakTransaction *self,
                                        gboolean            no_interaction)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  if (no_interaction == flatpak_transaction_get_no_interaction (self))
    return;

  flatpak_dir_set_no_interaction (priv->dir, no_interaction);
  g_object_notify (G_OBJECT (self), "no-interaction");
}

/**
 * flatpak_transaction_set_force_uninstall:
 * @self: a #FlatpakTransaction
 * @force_uninstall: whether to force-uninstall refs
 *
 * Sets whether the transaction should uninstall files even
 * if they're used by a running application.
 */
void
flatpak_transaction_set_force_uninstall (FlatpakTransaction *self,
                                         gboolean            force_uninstall)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  priv->force_uninstall = force_uninstall;
}

/**
 * flatpak_transaction_set_default_arch:
 * @self: a #FlatpakTransaction
 * @arch: the arch to make default
 *
 * Sets the architecture to default to where it is unspecified.
 */
void
flatpak_transaction_set_default_arch (FlatpakTransaction *self,
                                      const char         *arch)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  g_free (priv->default_arch);
  priv->default_arch = g_strdup (arch);
}

/**
 * flatpak_transaction_set_include_unused_uninstall_ops:
 * @self: a #FlatpakTransaction
 * @include_unused_uninstall_ops: whether to include unused uninstall ops
 *
 * When this is set to %TRUE, Flatpak will add uninstall operations to the
 * transaction for each runtime it considers unused. This is used by the
 * "update" CLI command to garbage collect runtimes and free disk space.
 *
 * No guarantees are made about the exact hueristic used; e.g. only end-of-life
 * unused runtimes may be uninstalled with this set. To see the full list of
 * unused runtimes in an installation, use
 * flatpak_installation_list_unused_refs().
 *
 * Since: 1.9.1
 */
void
flatpak_transaction_set_include_unused_uninstall_ops (FlatpakTransaction *self,
                                                      gboolean            include_unused_uninstall_ops)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  priv->include_unused_uninstall_ops = include_unused_uninstall_ops;
}

/**
 * flatpak_transaction_get_include_unused_uninstall_ops:
 * @self: a #FlatpakTransaction
 *
 * Gets the value set by
 * flatpak_transaction_set_include_unused_uninstall_ops().
 *
 * Returns: %TRUE if include_unused_uninstall_ops is set, %FALSE otherwise
 *
 * Since: 1.9.1
 */
gboolean
flatpak_transaction_get_include_unused_uninstall_ops (FlatpakTransaction *self)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  return priv->include_unused_uninstall_ops;
}

/**
 * flatpak_transaction_set_auto_install_sdk:
 * @self: a #FlatpakTransaction
 * @auto_install_sdk: whether to auto install SDKs for apps
 *
 * When this is set to %TRUE, Flatpak will automatically install the SDK for
 * each app currently being installed or updated. Does nothing if an uninstall
 * is taking place.
 *
 * Since: 1.13.3
 */
void
flatpak_transaction_set_auto_install_sdk (FlatpakTransaction *self,
                                          gboolean            auto_install_sdk)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  priv->auto_install_sdk = auto_install_sdk;
}

/**
 * flatpak_transaction_get_auto_install_sdk:
 * @self: a #FlatpakTransaction
 *
 * Gets the value set by
 * flatpak_transaction_set_auto_install_sdk().
 *
 * Returns: %TRUE if auto_install_sdk is set, %FALSE otherwise
 *
 * Since: 1.13.3
 */
gboolean
flatpak_transaction_get_auto_install_sdk (FlatpakTransaction *self)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  return priv->auto_install_sdk;
}

/**
 * flatpak_transaction_set_auto_install_debug:
 * @self: a #FlatpakTransaction
 * @auto_install_debug: whether to auto install debug info for apps
 *
 * When this is set to %TRUE, Flatpak will automatically install the debug info
 * for each app currently being installed or updated, as well as its
 * dependencies. Does nothing if an uninstall is taking place.
 *
 * Since: 1.13.3
 */
void
flatpak_transaction_set_auto_install_debug (FlatpakTransaction *self,
                                            gboolean            auto_install_debug)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  priv->auto_install_debug = auto_install_debug;
}

/**
 * flatpak_transaction_get_auto_install_debug:
 * @self: a #FlatpakTransaction
 *
 * Gets the value set by
 * flatpak_transaction_set_auto_install_debug().
 *
 * Returns: %TRUE if auto_install_debug is set, %FALSE otherwise
 *
 * Since: 1.13.3
 */
gboolean
flatpak_transaction_get_auto_install_debug (FlatpakTransaction *self)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  return priv->auto_install_debug;
}

static FlatpakTransactionOperation *
flatpak_transaction_get_last_op_for_ref (FlatpakTransaction *self,
                                         FlatpakDecomposed *ref)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  FlatpakTransactionOperation *op;

  op = g_hash_table_lookup (priv->last_op_for_ref, ref);

  return op;
}

static char *
subpaths_to_string (const char **subpaths)
{
  GString *s = NULL;
  int i;

  if (subpaths == NULL)
    return g_strdup ("[$old]");

  if (*subpaths == 0)
    return g_strdup ("[*]");

  s = g_string_new ("[");
  for (i = 0; subpaths[i] != NULL; i++)
    {
      if (i != 0)
        g_string_append (s, ", ");
      g_string_append (s, subpaths[i]);
    }
  g_string_append (s, "]");

  return g_string_free (s, FALSE);
}

static const char *
kind_to_str (FlatpakTransactionOperationType kind)
{
  switch ((int) kind)
    {
    case FLATPAK_TRANSACTION_OPERATION_INSTALL:
      return "install";

    case FLATPAK_TRANSACTION_OPERATION_UPDATE:
      return "update";

    case FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE:
      return "install/update";

    case FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE:
      return "install bundle";

    case FLATPAK_TRANSACTION_OPERATION_UNINSTALL:
      return "uninstall";

    case FLATPAK_TRANSACTION_OPERATION_LAST_TYPE:
    default:
      return "unknown";
    }
}

FlatpakRemoteState *
flatpak_transaction_ensure_remote_state (FlatpakTransaction             *self,
                                         FlatpakTransactionOperationType kind,
                                         const char                     *remote,
                                         const char                     *opt_arch,
                                         GError                        **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(FlatpakRemoteState) state = NULL;
  FlatpakRemoteState *cached_state;

  /* We don't cache local-only states, as we might later need the same state with non-local state */
  if (transaction_is_local_only (self, kind))
    return flatpak_dir_get_remote_state_local_only (priv->dir, remote, NULL, error);

  cached_state = g_hash_table_lookup (priv->remote_states, remote);
  if (cached_state)
    state = flatpak_remote_state_ref (cached_state);
  else
    {
      state = flatpak_dir_get_remote_state_optional (priv->dir, remote, FALSE, NULL, error);
      if (state == NULL)
        return NULL;

      g_hash_table_insert (priv->remote_states, state->remote_name, flatpak_remote_state_ref (state));

      for (int i = 0; i < priv->extra_sideload_repos->len; i++)
        {
          const char *path = g_ptr_array_index (priv->extra_sideload_repos, i);
          g_autoptr(GFile) f = g_file_new_for_path (path);
          flatpak_remote_state_add_sideload_dir (state, f);
        }
    }

  if (opt_arch != NULL &&
      !flatpak_remote_state_ensure_subsummary (state, priv->dir, opt_arch, FALSE, NULL, error))
    return FALSE;

  return g_steal_pointer (&state);
}

static gboolean
kind_compatible (FlatpakTransactionOperationType a,
                 FlatpakTransactionOperationType b,
                 gboolean                        b_is_rebase)
{
  if (a == b)
    return TRUE;

  if (a == FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE &&
      (b == FLATPAK_TRANSACTION_OPERATION_INSTALL ||
       b == FLATPAK_TRANSACTION_OPERATION_UPDATE))
    return TRUE;

  if (b == FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE &&
      (a == FLATPAK_TRANSACTION_OPERATION_INSTALL ||
       a == FLATPAK_TRANSACTION_OPERATION_UPDATE))
    return TRUE;

  /* If b is a rebase, the only reason it exists is so that the ref's previous-ids can be
     updated. Therefore, it can be folded into any other install or update operation. */
  if (b_is_rebase &&
      (a == FLATPAK_TRANSACTION_OPERATION_INSTALL ||
       a == FLATPAK_TRANSACTION_OPERATION_UPDATE ||
       a == FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE))
    return TRUE;

  return FALSE;
}

static FlatpakTransactionOperation *
flatpak_transaction_add_op (FlatpakTransaction             *self,
                            const char                     *remote,
                            FlatpakDecomposed              *ref,
                            const char                    **subpaths,
                            const char                    **previous_ids,
                            const char                     *commit,
                            GFile                          *bundle,
                            FlatpakTransactionOperationType kind,
                            gboolean                        pin_on_deploy)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  FlatpakTransactionOperation *op;
  g_autofree char *subpaths_str = NULL;

  subpaths_str = subpaths_to_string (subpaths);
  g_info ("Transaction: %s %s:%s%s%s%s",
          kind_to_str (kind), remote, flatpak_decomposed_get_ref (ref),
          commit != NULL ? "@" : "",
          commit != NULL ? commit : "",
          subpaths_str);

  op = flatpak_transaction_get_last_op_for_ref (self, ref);
  /* If previous_ids is given, then this is a rebase operation. */
  if (op != NULL && kind_compatible (kind, op->kind, previous_ids != NULL))
    {
      g_auto(GStrv) old_subpaths = NULL;
      g_auto(GStrv) old_previous_ids = NULL;

      old_subpaths = op->subpaths;
      op->subpaths = flatpak_subpaths_merge (old_subpaths, (char **) subpaths);

      old_previous_ids = op->previous_ids;
      op->previous_ids = flatpak_strv_merge (old_previous_ids, (char **) previous_ids);

      return op;
    }

  op = flatpak_transaction_operation_new (remote, ref, subpaths, previous_ids,
                                          commit, bundle, kind, pin_on_deploy);
  g_hash_table_insert (priv->last_op_for_ref, flatpak_decomposed_ref (ref), op);

  priv->ops = g_list_prepend (priv->ops, op);

  priv->needs_resolve = TRUE;

  return op;
}

static void
run_operation_before (FlatpakTransactionOperation *op,
                      FlatpakTransactionOperation *before_this,
                      int                          prio)
{
  if (op == before_this)
    return; /* Don't cause unnecessary loops */
  op->run_before_ops = g_list_prepend (op->run_before_ops, before_this);
  before_this->run_after_count++;
  before_this->run_after_prio = MAX (before_this->run_after_prio, prio);
}

static void
run_operation_last (FlatpakTransactionOperation *op)
{
  op->run_last = TRUE;
}

static gboolean
op_get_related (FlatpakTransaction           *self,
                FlatpakTransactionOperation  *op,
                GPtrArray                   **out_related,
                GError                      **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(FlatpakRemoteState) state = NULL;
  g_autoptr(GPtrArray) related = NULL;
  g_autoptr(GError) related_error = NULL;

  if (op->kind != FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
    {
      state = flatpak_transaction_ensure_remote_state (self, op->kind, op->remote, NULL, error);
      if (state == NULL)
        return FALSE;
    }

  if (op->resolved_metakey == NULL)
    {
      g_info ("no resolved metadata for related to %s", flatpak_decomposed_get_ref (op->ref));
      return TRUE;
    }

  if (transaction_is_local_only (self, op->kind))
    related = flatpak_dir_find_local_related_for_metadata (priv->dir, op->ref,
                                                           NULL, /* remote could differ from op->remote */
                                                           op->resolved_metakey,
                                                           NULL, &related_error);
  else
    related = flatpak_dir_find_remote_related_for_metadata (priv->dir, state, op->ref,
                                                            op->resolved_metakey, NULL, &related_error);

  if (related_error != NULL)
    g_message (_("Warning: Problem looking for related refs: %s"), related_error->message);

  if (out_related)
    *out_related = g_steal_pointer (&related);

  return TRUE;
}

static gboolean
add_related (FlatpakTransaction          *self,
             FlatpakTransactionOperation *op,
             GError                     **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(GPtrArray) related = NULL;
  int i;

  if (priv->disable_related)
    return TRUE;

  if (!op_get_related (self, op, &related, error))
    return FALSE;

  if (related == NULL)
    return TRUE;

  if (op->kind == FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
    {
      for (i = 0; i < related->len; i++)
        {
          FlatpakRelated *rel = g_ptr_array_index (related, i);
          FlatpakTransactionOperation *related_op;

          if (!rel->delete)
            continue;

          if (priv->no_deploy)
            {
              g_info ("Skipping uninstallation of %s for no deploy transaction",
                      flatpak_decomposed_get_ref (rel->ref));
              continue;
            }

          related_op = flatpak_transaction_add_op (self, rel->remote, rel->ref,
                                                   NULL, NULL, NULL, NULL,
                                                   FLATPAK_TRANSACTION_OPERATION_UNINSTALL,
                                                   FALSE);
          related_op->non_fatal = TRUE;
          related_op->fail_if_op_fails = op;
          flatpak_transaction_operation_add_related_to_op (related_op, op);
          run_operation_before (op, related_op, 1);
        }
    }
  else /* install or update */
    {
      for (i = 0; i < related->len; i++)
        {
          FlatpakRelated *rel = g_ptr_array_index (related, i);
          FlatpakTransactionOperation *related_op;
          gboolean download = rel->download;

          if (!download)
            {
              g_autofree char *id = flatpak_decomposed_dup_id (rel->ref);
              if (priv->auto_install_debug && g_str_has_suffix (id, ".Debug"))
                download = TRUE;
            }

          if (!download)
            continue;

          related_op = flatpak_transaction_add_op (self, rel->remote, rel->ref,
                                                   (const char **) rel->subpaths,
                                                   NULL, NULL, NULL,
                                                   FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE,
                                                   FALSE);
          related_op->non_fatal = TRUE;
          related_op->fail_if_op_fails = op;
          flatpak_transaction_operation_add_related_to_op (related_op, op);
          run_operation_before (related_op, op, 1);
        }
    }

  return TRUE;
}

typedef struct {
  FlatpakDir *dir;
  const char *prioritized_remote;
} RemoteSortData;

static gint
cmp_remote_with_prioritized (gconstpointer a,
                             gconstpointer b,
                             gpointer      user_data)
{
  RemoteSortData *rsd = user_data;
  FlatpakDir *self = rsd->dir;
  const char *a_name = *(const char **) a;
  const char *b_name = *(const char **) b;
  int prio_a, prio_b;

  prio_a = flatpak_dir_get_remote_prio (self, a_name);
  prio_b = flatpak_dir_get_remote_prio (self, b_name);

  /* Here we are assuming the array is already sorted by cmp_remote() and only
   * putting a particular remote at the top of its priority level */
  if (prio_b != prio_a)
    return prio_b - prio_a;
  else
    {
      if (strcmp (a_name, rsd->prioritized_remote) == 0)
        return -1;
      if (strcmp (b_name, rsd->prioritized_remote) == 0)
        return 1;
    }

  return 0;
}

static char **
search_for_dependency (FlatpakTransaction  *self,
                       char               **remotes,
                       FlatpakDecomposed   *runtime_ref,
                       GCancellable        *cancellable,
                       GError             **error)
{
  g_autoptr(GPtrArray) found = g_ptr_array_new_with_free_func (g_free);
  int i;
  g_autofree char *arch = flatpak_decomposed_dup_arch (runtime_ref);

  for (i = 0; remotes != NULL && remotes[i] != NULL; i++)
    {
      const char *remote = remotes[i];
      g_autoptr(GError) local_error = NULL;
      g_autoptr(FlatpakRemoteState) state = NULL;

      state = flatpak_transaction_ensure_remote_state (self, FLATPAK_TRANSACTION_OPERATION_INSTALL, remote, arch, &local_error);
      if (state == NULL)
        {
          g_info ("Can't get state for remote %s, ignoring: %s", remote, local_error->message);
          continue;
        }

      if (flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (runtime_ref), NULL, NULL, NULL, NULL, NULL))
        g_ptr_array_add (found, g_strdup (remote));
    }

  g_ptr_array_add (found, NULL);

  return (char **) g_ptr_array_free (g_steal_pointer (&found), FALSE);
}

static char **
search_for_local_dependency (FlatpakTransaction *self,
                             char              **remotes,
                             FlatpakDecomposed  *runtime_ref,
                             GCancellable       *cancellable,
                             GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(GPtrArray) found = g_ptr_array_new_with_free_func (g_free);
  int i;

  for (i = 0; remotes != NULL && remotes[i] != NULL; i++)
    {
      const char *remote = remotes[i];
      g_autofree char *commit = NULL;

      commit = flatpak_dir_read_latest (priv->dir, remote, flatpak_decomposed_get_ref (runtime_ref), NULL, NULL, NULL);
      if (commit != NULL)
        g_ptr_array_add (found, g_strdup (remote));
    }

  g_ptr_array_add (found, NULL);

  return (char **) g_ptr_array_free (g_steal_pointer (&found), FALSE);
}

static char *
find_runtime_remote (FlatpakTransaction             *self,
                     FlatpakDecomposed              *app_ref,
                     const char                     *app_remote,
                     FlatpakDecomposed              *runtime_ref,
                     FlatpakTransactionOperationType source_kind,
                     GCancellable                   *cancellable,
                     GError                        **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_auto(GStrv) all_remotes = NULL;
  g_auto(GStrv) found_remotes = NULL;
  const char *app_pref;
  const char *runtime_pref;
  RemoteSortData rsd = { NULL };
  int res = -1;

  all_remotes = flatpak_dir_list_dependency_remotes (priv->dir, cancellable, error);
  if (all_remotes == NULL)
    return NULL;

  /* Put @app_remote before the others at its priority level */
  rsd.dir = priv->dir;
  rsd.prioritized_remote = app_remote;
  g_qsort_with_data (all_remotes, g_strv_length (all_remotes), sizeof (char *),
                     cmp_remote_with_prioritized, &rsd);


  app_pref = flatpak_decomposed_get_pref (app_ref);
  runtime_pref = flatpak_decomposed_get_pref (runtime_ref);

  /* Here we are passing along app_remote so it gets priority */
  if (transaction_is_local_only (self, source_kind))
    found_remotes = search_for_local_dependency (self, all_remotes, runtime_ref, NULL, NULL);
  else
    found_remotes = search_for_dependency (self, all_remotes, runtime_ref, NULL, NULL);

  if (found_remotes == NULL || *found_remotes == NULL)
    {
      flatpak_fail_error (error, FLATPAK_ERROR_RUNTIME_NOT_FOUND,
                          _("The application %s requires the runtime %s which was not found"),
                          app_pref, runtime_pref);
      return NULL;
    }

  /* In the no-pull case, if only one local ref is available, assume that is the one because
     the user chose it interactively when pulling */
  if (priv->no_pull && g_strv_length (found_remotes) == 1)
    res = 0;
  else
    g_signal_emit (self, signals[CHOOSE_REMOTE_FOR_REF], 0, flatpak_decomposed_get_ref (app_ref), flatpak_decomposed_get_ref (runtime_ref), found_remotes, &res);

  if (res >= 0 && res < g_strv_length (found_remotes))
    return g_strdup (found_remotes[res]);

  flatpak_fail_error (error, FLATPAK_ERROR_RUNTIME_NOT_FOUND,
                      _("The application %s requires the runtime %s which is not installed"),
                      app_pref, runtime_pref);
  return NULL;
}

static FlatpakDecomposed *
op_get_runtime_ref (FlatpakTransactionOperation *op)
{
  g_autofree char *runtime_pref = NULL;
  FlatpakDecomposed *decomposed;

  if (!op->resolved_metakey)
    return NULL;

  /* Generally only app needs runtimes dependencies, not dependencies because you don't run extensions directly.
     However if the extension has extra data (and doesn't define NoRuntime) its also needed so we can run the
     apply-extra script. */
  if (flatpak_decomposed_is_app (op->ref))
    runtime_pref = g_key_file_get_string (op->resolved_metakey, "Application", "runtime", NULL);
  else if (g_key_file_has_group (op->resolved_metakey, "Extra Data") &&
           !g_key_file_get_boolean (op->resolved_metakey, "Extra Data", "NoRuntime", NULL))
    runtime_pref = g_key_file_get_string (op->resolved_metakey, "ExtensionOf", "runtime", NULL);

  if (runtime_pref == NULL)
    return NULL;

  decomposed = flatpak_decomposed_new_from_pref (FLATPAK_KINDS_RUNTIME, runtime_pref, NULL);
  if (decomposed == NULL)
    g_info ("Invalid runtime ref %s in metadata", runtime_pref);

  return decomposed;
}

static FlatpakDecomposed *
op_get_sdk_ref (FlatpakTransactionOperation *op)
{
  g_autofree char *sdk_pref = NULL;
  FlatpakDecomposed *decomposed;

  if (!op->resolved_metakey || !flatpak_decomposed_is_app (op->ref))
    return NULL;

  sdk_pref = g_key_file_get_string (op->resolved_metakey, "Application", "sdk", NULL);
  if (sdk_pref == NULL)
    return NULL;

  decomposed = flatpak_decomposed_new_from_pref (FLATPAK_KINDS_RUNTIME, sdk_pref, NULL);
  if (decomposed == NULL)
    g_info ("Invalid runtime ref %s in metadata", sdk_pref);

  return decomposed;
}

static gboolean
add_new_dep_op (FlatpakTransaction           *self,
                FlatpakTransactionOperation  *op,
                FlatpakDecomposed            *dep_ref,
                FlatpakTransactionOperation **dep_op,
                GError                      **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autofree char *dep_remote = NULL;

  if (!ref_is_installed (self, dep_ref))
    {
      g_info ("Installing dependency %s of %s", flatpak_decomposed_get_pref (dep_ref),
              flatpak_decomposed_get_pref (op->ref));
      dep_remote = find_runtime_remote (self, op->ref, op->remote, dep_ref, op->kind, NULL, error);
      if (dep_remote == NULL)
        return FALSE;

      *dep_op = flatpak_transaction_add_op (self, dep_remote, dep_ref, NULL, NULL, NULL, NULL,
                                            FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE, FALSE);
    }
  else
    {
      /* Update if in same dir */
      if (dir_ref_is_installed (priv->dir, dep_ref, &dep_remote, NULL))
        {
          g_info ("Updating dependency %s of %s", flatpak_decomposed_get_pref (dep_ref),
                  flatpak_decomposed_get_pref (op->ref));
          *dep_op = flatpak_transaction_add_op (self, dep_remote, dep_ref, NULL, NULL, NULL, NULL,
                                                FLATPAK_TRANSACTION_OPERATION_UPDATE, FALSE);
          (*dep_op)->non_fatal = TRUE;
        }
    }

  return TRUE;
}

static gboolean
add_deps (FlatpakTransaction          *self,
          FlatpakTransactionOperation *op,
          GError                     **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(FlatpakDecomposed) runtime_ref = NULL;
  FlatpakTransactionOperation *runtime_op = NULL;

  if (!op->resolved_metakey)
    return TRUE;

  runtime_ref = op_get_runtime_ref (op);
  if (runtime_ref == NULL)
    return TRUE;

  runtime_op = flatpak_transaction_get_last_op_for_ref (self, runtime_ref);

  if (op->kind == FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
    {
      /* If the runtime this app uses is already to be uninstalled, then this uninstall must happen before
         the runtime is uninstalled */
      if (runtime_op && runtime_op->kind == FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
        run_operation_before (op, runtime_op, 1);

      return TRUE;
    }

  if (priv->disable_deps)
    return TRUE;

  if (runtime_op == NULL)
    {
      if (!add_new_dep_op (self, op, runtime_ref, &runtime_op, error))
        return FALSE;
    }

  /* Install/Update the runtime before the app */
  if (runtime_op)
    {
      if (runtime_op->kind == FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
        return flatpak_fail_error (error, FLATPAK_ERROR_RUNTIME_USED,
                                   _("Can't uninstall %s which is needed by %s"),
                                   flatpak_decomposed_get_pref (runtime_op->ref), flatpak_decomposed_get_pref (op->ref));

      op->fail_if_op_fails = runtime_op;
      flatpak_transaction_operation_add_related_to_op (runtime_op, op);
      run_operation_before (runtime_op, op, 2);
    }

  if (priv->auto_install_sdk)
    {
      g_autoptr(FlatpakDecomposed) sdk_ref = NULL;

      sdk_ref = op_get_sdk_ref (op);
      if (sdk_ref != NULL)
        {
          FlatpakTransactionOperation *sdk_op = flatpak_transaction_get_last_op_for_ref (self, sdk_ref);
          if (sdk_op == NULL)
            {
              if (!add_new_dep_op (self, op, sdk_ref, &sdk_op, error))
                return FALSE;
            }

          if (sdk_op->kind != FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
            {
              flatpak_transaction_operation_add_related_to_op (sdk_op, op);
              run_operation_before (sdk_op, op, 2);
            }
        }
    }

  return TRUE;
}

/* @out_op may return %NULL even when this function returns %TRUE. It’s (transfer none). */
static gboolean
flatpak_transaction_add_ref (FlatpakTransaction             *self,
                             const char                     *remote,
                             FlatpakDecomposed              *ref,
                             const char                    **subpaths,
                             const char                    **previous_ids,
                             const char                     *commit,
                             FlatpakTransactionOperationType kind,
                             GFile                          *bundle,
                             const char                     *external_metadata,
                             gboolean                        pin_on_deploy,
                             FlatpakTransactionOperation   **out_op,
                             GError                        **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autofree char *origin = NULL;
  g_auto(GStrv) new_subpaths = NULL;
  const char *pref;
  g_autofree char *origin_remote = NULL;
  g_autoptr(FlatpakRemoteState) state = NULL;
  FlatpakTransactionOperation *op;

  if (out_op != NULL)
    *out_op = NULL;

  if (remote_name_is_file (remote))
    {
      gboolean changed_config;
      g_autofree char *id = flatpak_decomposed_dup_id (ref);
      origin_remote = flatpak_dir_create_origin_remote (priv->dir,
                                                        remote, /* uri */
                                                        id,
                                                        "Local repo",
                                                        flatpak_decomposed_get_ref (ref),
                                                        NULL,
                                                        NULL,
                                                        &changed_config,
                                                        NULL, error);
      if (origin_remote == NULL)
        return FALSE;

      /* Reload changed configuration */
      if (changed_config)
        flatpak_installation_drop_caches (priv->installation, NULL, NULL);

      g_ptr_array_add (priv->added_origin_remotes, g_strdup (origin_remote));

      remote = origin_remote;
    }

  pref = flatpak_decomposed_get_pref (ref);

  /* install or update */
  if (kind == FLATPAK_TRANSACTION_OPERATION_UPDATE)
    {
      g_autoptr(GBytes) deploy_data = NULL;

      if (!dir_ref_is_installed (priv->dir, ref, &origin, &deploy_data))
        return flatpak_fail_error (error, FLATPAK_ERROR_NOT_INSTALLED,
                                   _("%s not installed"), pref);

      if (flatpak_dir_get_remote_disabled (priv->dir, origin))
        {
          g_info (_("Remote %s disabled, ignoring %s update"), origin, pref);
          return TRUE;
        }
      remote = origin;

      if (subpaths == NULL)
        {
          g_autofree const char **old_subpaths = flatpak_deploy_data_get_subpaths (deploy_data);

          /* As stated in the documentation for flatpak_transaction_add_update(),
           * for locale extensions we merge existing subpaths with the set of
           * configured languages, to match the behavior of add_related().
           */
          if (flatpak_decomposed_id_has_suffix (ref, ".Locale"))
            {
              g_auto(GStrv) extra_subpaths = flatpak_dir_get_locale_subpaths (priv->dir);
              new_subpaths = flatpak_subpaths_merge ((char **)old_subpaths, extra_subpaths);
            }
          else
            {
              /* Otherwise we resolve to the current subpaths here so we can know in operation-done what subpaths will be pulled */
              new_subpaths = g_strdupv ((char **)old_subpaths);
            }
          subpaths = (const char **)new_subpaths;
        }
    }
  else if (kind == FLATPAK_TRANSACTION_OPERATION_INSTALL)
    {
      if (!priv->reinstall &&
          dir_ref_is_installed (priv->dir, ref, &origin, NULL))
        {
          if (g_strcmp0 (remote, origin) == 0)
            return flatpak_fail_error (error, FLATPAK_ERROR_ALREADY_INSTALLED,
                                       _("%s is already installed"), pref);
          else
            return flatpak_fail_error (error, FLATPAK_ERROR_DIFFERENT_REMOTE,
                                       _("%s is already installed from remote %s"),
                                       pref, origin);
        }
    }
  else if (kind == FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
    {
      /* Skip uninstall for no deploy transactions. */
      if (priv->no_deploy)
        {
          g_info ("Skipping uninstallation of %s for no deploy transaction", pref);
          return TRUE;
        }

      if (!dir_ref_is_installed (priv->dir, ref, &origin, NULL))
        return flatpak_fail_error (error, FLATPAK_ERROR_NOT_INSTALLED,
                                   _("%s not installed"), pref);

      remote = origin;
    }

  /* This should have been passed in or found out above */
  g_assert (remote != NULL);

  /* We don't need remote state for an uninstall, and we don't want a missing
   * remote to be fatal */
  if (kind != FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
    {
      g_autofree char *arch = flatpak_decomposed_dup_arch (ref);

      state = flatpak_transaction_ensure_remote_state (self, kind, remote, arch, error);
      if (state == NULL)
        return FALSE;
    }

  op = flatpak_transaction_add_op (self, remote, ref, subpaths, previous_ids,
                                   commit, bundle, kind, pin_on_deploy);

  if (external_metadata)
    op->external_metadata = g_bytes_new (external_metadata, strlen (external_metadata));

  if (out_op != NULL)
    *out_op = op;

  return TRUE;
}

/**
 * flatpak_transaction_add_install:
 * @self: a #FlatpakTransaction
 * @remote: the name of the remote
 * @ref: the ref
 * @subpaths: (nullable) (array zero-terminated=1): subpaths to install, or the
 *  empty list or %NULL to pull all subpaths
 * @error: return location for a #GError
 *
 * Adds installing the given ref to this transaction.
 *
 * The @remote can either be a configured remote of the installation,
 * or a file:// uri pointing at a local repository to install from,
 * in which case an origin remote is created.
 *
 * Returns: %TRUE on success; %FALSE with @error set on failure.
 */
gboolean
flatpak_transaction_add_install (FlatpakTransaction *self,
                                 const char         *remote,
                                 const char         *ref,
                                 const char        **subpaths,
                                 GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(FlatpakDecomposed) decomposed = NULL;
  const char *all_paths[] = { NULL };
  gboolean pin_on_deploy;

  g_return_val_if_fail (ref != NULL, FALSE);
  g_return_val_if_fail (remote != NULL, FALSE);

  decomposed = flatpak_decomposed_new_from_ref (ref, error);
  if (decomposed == NULL)
    return FALSE;

  /* If we install with no special args pull all subpaths */
  if (subpaths == NULL)
    subpaths = all_paths;

  pin_on_deploy = flatpak_decomposed_is_runtime (decomposed) && !priv->disable_auto_pin;

  if (!flatpak_transaction_add_ref (self, remote, decomposed, subpaths, NULL, NULL,
                                    FLATPAK_TRANSACTION_OPERATION_INSTALL,
                                    NULL, NULL, pin_on_deploy, NULL, error))
    return FALSE;

  return TRUE;
}

/**
 * flatpak_transaction_add_rebase:
 * @self: a #FlatpakTransaction
 * @remote: the name of the remote
 * @ref: the ref
 * @subpaths: (nullable): the subpaths to include, or %NULL to install the complete ref
 * @previous_ids: (nullable) (array zero-terminated=1): Previous ids to add to the
 *     given ref. These should simply be the ids, not the full ref names (e.g. org.foo.Bar,
 *     not org.foo.Bar/x86_64/master).
 * @error: return location for a #GError
 *
 * Adds updating the @previous_ids of the given ref to this transaction, via either
 * installing the @ref if it was not already present or updating it. This will
 * treat @ref as the result of following an eol-rebase, and data migration from
 * the refs in @previous_ids will be set up.
 *
 * If you want to rebase the ref and uninstall the old version of it, consider
 * using flatpak_transaction_add_rebase_and_uninstall() instead. It will add
 * appropriate dependencies between the rebase and uninstall operations.
 *
 * See flatpak_transaction_add_install() for a description of @remote.
 *
 * Returns: %TRUE on success; %FALSE with @error set on failure.
 * Since: 1.3.3.
 */
gboolean
flatpak_transaction_add_rebase (FlatpakTransaction *self,
                                const char         *remote,
                                const char         *ref,
                                const char        **subpaths,
                                const char        **previous_ids,
                                GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  const char *all_paths[] = { NULL };
  g_autoptr(FlatpakDecomposed) decomposed = NULL;
  g_autofree char *installed_origin = NULL;

  g_return_val_if_fail (ref != NULL, FALSE);
  g_return_val_if_fail (remote != NULL, FALSE);
  /* flatpak_transaction_add_rebase without previous_ids doesn't make sense */
  g_return_val_if_fail (previous_ids != NULL, FALSE);

  decomposed = flatpak_decomposed_new_from_ref (ref, error);
  if (decomposed == NULL)
    return FALSE;

  /* If we install with no special args pull all subpaths */
  if (subpaths == NULL)
    subpaths = all_paths;

  if (dir_ref_is_installed (priv->dir, decomposed, &installed_origin, NULL))
    remote = installed_origin;

  return flatpak_transaction_add_ref (self, remote, decomposed, subpaths, previous_ids, NULL, FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE, NULL, NULL, FALSE, NULL, error);
}

/**
 * flatpak_transaction_add_rebase_and_uninstall:
 * @self: a #FlatpakTransaction
 * @remote: the name of the remote
 * @new_ref: the ref to rebase to
 * @old_ref: the ref to uninstall
 * @subpaths: (nullable): the subpaths to include, or %NULL to install the complete ref
 * @previous_ids: (nullable) (array zero-terminated=1): Previous ids to add to the
 *     given ref. These should simply be the ids, not the full ref names (e.g. org.foo.Bar,
 *     not org.foo.Bar/x86_64/master).
 * @error: return location for a #GError
 *
 * Adds updating the @previous_ids of the given @new_ref to this transaction,
 * via either installing the @new_ref if it was not already present or updating
 * it. This will treat @new_ref as the result of following an eol-rebase, and
 * data migration from the refs in @previous_ids will be set up.
 *
 * Also adds an operation to uninstall @old_ref to this transaction. This
 * operation will only be run if the operation to install/update @new_ref
 * succeeds.
 *
 * If @old_ref is not already installed (which can happen if requesting to
 * install an EOLed app, rather than update one which is already installed), the
 * uninstall operation will silently not be added, and this function will behave
 * similarly to flatpak_transaction_add_rebase().
 *
 * See flatpak_transaction_add_install() for a description of @remote.
 *
 * Returns: %TRUE on success; %FALSE with @error set on failure.
 * Since: 1.15.4
 */
gboolean
flatpak_transaction_add_rebase_and_uninstall (FlatpakTransaction  *self,
                                              const char          *remote,
                                              const char          *new_ref,
                                              const char          *old_ref,
                                              const char         **subpaths,
                                              const char         **previous_ids,
                                              GError             **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  const char *all_paths[] = { NULL };
  g_autoptr(FlatpakDecomposed) old_decomposed = NULL;
  g_autoptr(FlatpakDecomposed) new_decomposed = NULL;
  g_autofree char *installed_origin = NULL;
  g_autoptr(GError) local_error = NULL;
  FlatpakTransactionOperation *rebase_op = NULL, *uninstall_op = NULL;

  g_return_val_if_fail (new_ref != NULL, FALSE);
  g_return_val_if_fail (old_ref != NULL, FALSE);
  g_return_val_if_fail (remote != NULL, FALSE);
  /* flatpak_transaction_add_rebase_and_uninstall() without previous_ids doesn't make sense */
  g_return_val_if_fail (previous_ids != NULL, FALSE);

  new_decomposed = flatpak_decomposed_new_from_ref (new_ref, error);
  if (new_decomposed == NULL)
    return FALSE;

  old_decomposed = flatpak_decomposed_new_from_ref (old_ref, error);
  if (old_decomposed == NULL)
    return FALSE;

  /* If we install with no special args pull all subpaths */
  if (subpaths == NULL)
    subpaths = all_paths;

  if (dir_ref_is_installed (priv->dir, new_decomposed, &installed_origin, NULL))
    remote = installed_origin;

  /* Add the install/update and uninstall ops. */
  if (!flatpak_transaction_add_ref (self, remote, new_decomposed, subpaths,
                                    previous_ids, NULL,
                                    FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE,
                                    NULL, NULL, FALSE, &rebase_op, error))
    return FALSE;

  if (!flatpak_transaction_add_ref (self, NULL, old_decomposed, NULL, NULL, NULL,
                                    FLATPAK_TRANSACTION_OPERATION_UNINSTALL,
                                    NULL, NULL, FALSE, &uninstall_op, &local_error))
    {
      /* If the user is trying to install an eol-rebased app from scratch, the
       * @old_ref can’t be uninstalled because it’s not installed already.
       * Silently ignore that. */
      if (g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED))
        {
          g_clear_error (&local_error);
        }
      else
        {
          g_propagate_error (error, g_steal_pointer (&local_error));
          return FALSE;
        }
    }

  /* Link the ops together so that the install/update is done first, and if
   * that fails then the uninstall is skipped. @uninstall_op might be %NULL even
   * if the flatpak_transaction_add_ref() call succeeded above, as this might be
   * a no-deploy transaction. */
  if (uninstall_op != NULL)
    {
      uninstall_op->non_fatal = TRUE;
      uninstall_op->fail_if_op_fails = rebase_op;
      flatpak_transaction_operation_add_related_to_op (uninstall_op, rebase_op);
      run_operation_before (rebase_op, uninstall_op, 1);
    }

  return TRUE;
}

/**
 * flatpak_transaction_add_install_bundle:
 * @self: a #FlatpakTransaction
 * @file: a #GFile that is an flatpak bundle
 * @gpg_data: (nullable): GPG key with which to check bundle signatures, or
 *  %NULL to use the key embedded in the bundle (if any)
 * @error: return location for a #GError
 *
 * Adds installing the given bundle to this transaction.
 *
 * Returns: %TRUE on success; %FALSE with @error set on failure.
 */
gboolean
flatpak_transaction_add_install_bundle (FlatpakTransaction *self,
                                        GFile              *file,
                                        GBytes             *gpg_data,
                                        GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  priv->bundles = g_list_append (priv->bundles, bundle_data_new (file, gpg_data));

  return TRUE;
}

/**
 * flatpak_transaction_add_install_flatpakref:
 * @self: a #FlatpakTransaction
 * @flatpakref_data: data from a flatpakref file
 * @error: return location for a #GError
 *
 * Adds installing the given flatpakref to this transaction.
 *
 * Returns: %TRUE on success; %FALSE with @error set on failure.
 */
gboolean
flatpak_transaction_add_install_flatpakref (FlatpakTransaction *self,
                                            GBytes             *flatpakref_data,
                                            GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(GKeyFile) keyfile = g_key_file_new ();
  g_autoptr(GError) local_error = NULL;

  g_return_val_if_fail (flatpakref_data != NULL, FALSE);

  if (!g_key_file_load_from_data (keyfile, g_bytes_get_data (flatpakref_data, NULL),
                                  g_bytes_get_size (flatpakref_data),
                                  0, &local_error))
    return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Invalid .flatpakref: %s"), local_error->message);

  priv->flatpakrefs = g_list_append (priv->flatpakrefs, g_steal_pointer (&keyfile));

  return TRUE;
}

/**
 * flatpak_transaction_add_update:
 * @self: a #FlatpakTransaction
 * @ref: the ref
 * @subpaths: (nullable) (array zero-terminated=1): subpaths to install; %NULL
 *  to use the current set plus the set of configured languages, or
 *  `{ NULL }` or `{ "", NULL }` to pull all subpaths.
 * @commit: (nullable): the commit to update to, or %NULL to use the latest
 * @error: return location for a #GError
 *
 * Adds updating the given ref to this transaction.
 *
 * Returns: %TRUE on success; %FALSE with @error set on failure.
 */
gboolean
flatpak_transaction_add_update (FlatpakTransaction *self,
                                const char         *ref,
                                const char        **subpaths,
                                const char         *commit,
                                GError            **error)
{
  const char *all_paths[] = { NULL };
  g_autoptr(FlatpakDecomposed) decomposed = NULL;

  g_return_val_if_fail (ref != NULL, FALSE);

  /* If specify an empty subpath, that means all subpaths */
  if (subpaths != NULL && subpaths[0] != NULL && subpaths[0][0] == 0)
    subpaths = all_paths;

  decomposed = flatpak_decomposed_new_from_ref (ref, error);
  if (decomposed == NULL)
    return FALSE;

  /* Note: we implement the merge when subpaths == NULL in flatpak_transaction_add_ref() */
  return flatpak_transaction_add_ref (self, NULL, decomposed, subpaths, NULL, commit, FLATPAK_TRANSACTION_OPERATION_UPDATE, NULL, NULL, FALSE, NULL, error);
}

/**
 * flatpak_transaction_add_uninstall:
 * @self: a #FlatpakTransaction
 * @ref: the ref
 * @error: return location for a #GError
 *
 * Adds uninstalling the given ref to this transaction. If the transaction is
 * set to not deploy updates, the request is ignored.
 *
 * Returns: %TRUE on success; %FALSE with @error set on failure.
 */
gboolean
flatpak_transaction_add_uninstall (FlatpakTransaction *self,
                                   const char         *ref,
                                   GError            **error)
{
  g_autoptr(FlatpakDecomposed) decomposed = NULL;

  g_return_val_if_fail (ref != NULL, FALSE);

  decomposed = flatpak_decomposed_new_from_ref (ref, error);
  if (decomposed == NULL)
    return FALSE;

  return flatpak_transaction_add_ref (self, NULL, decomposed, NULL, NULL, NULL, FLATPAK_TRANSACTION_OPERATION_UNINSTALL, NULL, NULL, FALSE, NULL, error);
}

static gboolean
flatpak_transaction_update_metadata (FlatpakTransaction *self,
                                     GCancellable       *cancellable,
                                     GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_auto(GStrv) remotes = NULL;
  int i;
  GList *l;
  gboolean some_updated = FALSE;
  g_autoptr(GHashTable) ht = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
  gboolean local_only = TRUE;

  /* Collect all dir+remotes used in this transaction */

  if (!flatpak_dir_migrate_config (priv->dir, &some_updated, cancellable, error))
    return FALSE;

  for (l = priv->ops; l != NULL; l = l->next)
    {
      FlatpakTransactionOperation *op = l->data;
      if (!g_hash_table_contains (ht, op->remote))
        g_hash_table_add (ht, g_strdup (op->remote));
      local_only = local_only && transaction_is_local_only (self, op->kind);
    }
  remotes = (char **) g_hash_table_get_keys_as_array (ht, NULL);
  g_hash_table_steal_all (ht); /* Move ownership to remotes */

  /* Bail early if the entire transaction is local-only, as in that case we
   * don’t need updated metadata. */
  if (local_only)
    return TRUE;

  /* Update metadata for said remotes */
  for (i = 0; remotes[i] != NULL; i++)
    {
      char *remote = remotes[i];
      gboolean updated = FALSE;
      g_autoptr(GError) my_error = NULL;
      g_autoptr(FlatpakRemoteState) state = flatpak_transaction_ensure_remote_state (self, FLATPAK_TRANSACTION_OPERATION_UPDATE, remote, NULL, NULL);

      g_info ("Looking for remote metadata updates for %s", remote);
      if (!flatpak_dir_update_remote_configuration (priv->dir, remote, state, &updated, cancellable, &my_error))
        g_info (_("Error updating remote metadata for '%s': %s"), remote, my_error->message);

      if (updated)
        {
          g_info ("Got updated metadata for %s", remote);
          some_updated = TRUE;
        }
    }

  if (some_updated)
    {
      /* Reload changed configuration */
      if (!flatpak_dir_recreate_repo (priv->dir, cancellable, error))
        return FALSE;

      flatpak_installation_drop_caches (priv->installation, NULL, NULL);

      /* These are potentially out of date now */
      g_hash_table_remove_all (priv->remote_states);
    }

  return TRUE;
}

static gboolean
flatpak_transaction_add_auto_install (FlatpakTransaction *self,
                                      GCancellable       *cancellable,
                                      GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_auto(GStrv) remotes = NULL;

  remotes = flatpak_dir_list_remotes (priv->dir, cancellable, error);
  if (remotes == NULL)
    return FALSE;

  /* Auto-add auto-download apps that are not already installed.
   * Try to avoid doing network i/o until we know its needed, as this
   * iterates over all configured remotes.
   */
  for (int i = 0; remotes[i] != NULL; i++)
    {
      char *remote = remotes[i];
      g_autoptr(FlatpakDecomposed) auto_install_ref = NULL;

      if (flatpak_dir_get_remote_disabled (priv->dir, remote))
        continue;

      auto_install_ref = flatpak_dir_get_remote_auto_install_authenticator_ref (priv->dir, remote);
      if (auto_install_ref != NULL)
        {
          g_autoptr(GError) local_error = NULL;
          g_autoptr(GFile) deploy = NULL;

          deploy = flatpak_dir_get_if_deployed (priv->dir, auto_install_ref, NULL, cancellable);
          if (deploy == NULL)
            {
              g_autoptr(FlatpakRemoteState) state = flatpak_transaction_ensure_remote_state (self, FLATPAK_TRANSACTION_OPERATION_UPDATE, remote, NULL, NULL);

              if (state != NULL &&
                  flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (auto_install_ref), NULL, NULL, NULL, NULL, NULL))
                {
                  g_info ("Auto adding install of %s from remote %s", flatpak_decomposed_get_ref (auto_install_ref), remote);

                  if (!flatpak_transaction_add_ref (self, remote, auto_install_ref, NULL, NULL, NULL,
                                                    FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE,
                                                    NULL, NULL, FALSE, NULL,
                                                    &local_error))
                    g_info ("Failed to add auto-install ref %s: %s", flatpak_decomposed_get_ref (auto_install_ref),
                             local_error->message);
                }
            }
        }
    }

  return TRUE;
}

static void
emit_new_op (FlatpakTransaction *self, FlatpakTransactionOperation *op, FlatpakTransactionProgress *progress)
{
  g_signal_emit (self, signals[NEW_OPERATION], 0, op, progress);
}

static void
emit_op_done (FlatpakTransaction          *self,
              FlatpakTransactionOperation *op,
              FlatpakTransactionResult     details)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autofree char *commit = NULL;

  if (priv->no_deploy)
    commit = flatpak_dir_read_latest (priv->dir, op->remote, flatpak_decomposed_get_ref (op->ref), NULL, NULL, NULL);
  else
    {
      g_autoptr(GBytes) deploy_data = flatpak_dir_get_deploy_data (priv->dir, op->ref, FLATPAK_DEPLOY_VERSION_ANY, NULL, NULL);
      if (deploy_data)
        commit = g_strdup (flatpak_deploy_data_get_commit (deploy_data));
    }

  g_signal_emit (self, signals[OPERATION_DONE], 0, op, commit, details);
}

static GBytes *
load_deployed_metadata (FlatpakTransaction *self, FlatpakDecomposed *ref, char **out_commit, char **out_remote)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(GFile) deploy_dir = NULL;
  g_autoptr(GFile) metadata_file = NULL;
  g_autofree char *metadata_contents = NULL;
  gsize metadata_contents_length;

  deploy_dir = flatpak_dir_get_if_deployed (priv->dir, ref, NULL, NULL);
  if (deploy_dir == NULL)
    return NULL;

  if (out_commit || out_remote)
    {
      g_autoptr(GBytes) deploy_data = NULL;
      deploy_data = flatpak_load_deploy_data (deploy_dir, ref,
                                              flatpak_dir_get_repo (priv->dir),
                                              FLATPAK_DEPLOY_VERSION_ANY, NULL, NULL);
      if (deploy_data == NULL)
        return NULL;

      if (out_commit)
        *out_commit = g_strdup (flatpak_deploy_data_get_commit (deploy_data));
      if (out_remote)
        *out_remote = g_strdup (flatpak_deploy_data_get_origin (deploy_data));
    }

  metadata_file = g_file_get_child (deploy_dir, "metadata");

  if (!g_file_load_contents (metadata_file, NULL, &metadata_contents, &metadata_contents_length, NULL, NULL))
    {
      g_info ("No metadata in local deploy of %s", flatpak_decomposed_get_ref (ref));
      return NULL;
    }

  return g_bytes_new_take (g_steal_pointer (&metadata_contents), metadata_contents_length);
}

static void
emit_eol_and_maybe_skip (FlatpakTransaction          *self,
                         FlatpakTransactionOperation *op)
{
  g_autofree char *id = NULL;
  const char *previous_ids[] = { NULL, NULL };

  if (op->skip || (!op->eol && !op->eol_rebase) ||
      op->kind == FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
    return;

  id = flatpak_decomposed_dup_id (op->ref);
  previous_ids[0] = id;

  g_signal_emit (self, signals[END_OF_LIFED_WITH_REBASE], 0, op->remote, flatpak_decomposed_get_ref (op->ref), op->eol, op->eol_rebase, previous_ids, &op->skip);
}

static gboolean
mark_op_resolved (FlatpakTransactionOperation *op,
                  const char                  *commit,
                  GFile                       *sideload_path,
                  GBytes                      *metadata,
                  GBytes                      *old_metadata,
                  GError                     **error)
{
  g_info ("marking op %s:%s resolved to %s", kind_to_str (op->kind), flatpak_decomposed_get_ref (op->ref), commit ? commit : "-");

  g_assert (op != NULL);

  g_assert (commit != NULL);

  op->resolved = TRUE;

  if (op->resolved_commit != commit)
    {
      g_free (op->resolved_commit); /* This is already set if we retry resolving to get a token, so free first */
      op->resolved_commit = g_strdup (commit);
    }

  if (sideload_path)
    op->resolved_sideload_path = g_object_ref (sideload_path);

  if (metadata)
    {
      g_autoptr(GKeyFile) metakey = g_key_file_new ();
      if (!g_key_file_load_from_bytes (metakey, metadata, G_KEY_FILE_NONE, NULL))
        return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA,
                                   "Metadata for %s is invalid", flatpak_decomposed_get_ref (op->ref));

      op->resolved_metadata = g_bytes_ref (metadata);
      op->resolved_metakey = g_steal_pointer (&metakey);
    }
  if (old_metadata)
    {
      g_autoptr(GKeyFile) metakey = g_key_file_new ();
      if (g_key_file_load_from_bytes (metakey, old_metadata, G_KEY_FILE_NONE, NULL))
        {
          op->resolved_old_metadata = g_bytes_ref (old_metadata);
          op->resolved_old_metakey = g_steal_pointer (&metakey);
        }
      else
        {
          /* This shouldn't happen, but a NULL old metadata is safe (all permisssions are considered new) */
          g_message ("Warning: Failed to parse old metadata for %s\n", flatpak_decomposed_get_ref (op->ref));
        }
    }

  return TRUE;
}

static gboolean
resolve_op_end (FlatpakTransaction *self,
                FlatpakTransactionOperation *op,
                const char *checksum,
                GFile *sideload_path,
                GBytes *metadata_bytes,
                GError **error)
{
  g_autoptr(GBytes) old_metadata_bytes = NULL;

  old_metadata_bytes = load_deployed_metadata (self, op->ref, NULL, NULL);
  if (!mark_op_resolved (op, checksum, sideload_path, metadata_bytes, old_metadata_bytes, error))
    return FALSE;
  emit_eol_and_maybe_skip (self, op);
  return TRUE;
 }


static gboolean
resolve_op_from_commit (FlatpakTransaction *self,
                        FlatpakTransactionOperation *op,
                        const char *checksum,
                        GFile *sideload_path,
                        GVariant *commit_data,
                        GError **error)
{
  g_autoptr(GBytes) metadata_bytes = NULL;
  g_autoptr(GVariant) commit_metadata = NULL;
  const char *xa_metadata = NULL;
  guint64 download_size = 0;
  guint64 installed_size = 0;

  commit_metadata = g_variant_get_child_value (commit_data, 0);
  g_variant_lookup (commit_metadata, "xa.metadata", "&s", &xa_metadata);
  if (xa_metadata == NULL)
    return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA,
                               "No xa.metadata in local commit %s ref %s",
                               checksum, flatpak_decomposed_get_ref (op->ref));

  metadata_bytes = g_bytes_new (xa_metadata, strlen (xa_metadata));

  if (g_variant_lookup (commit_metadata, "xa.download-size", "t", &download_size))
    op->download_size = GUINT64_FROM_BE (download_size);
  if (g_variant_lookup (commit_metadata, "xa.installed-size", "t", &installed_size))
    op->installed_size = GUINT64_FROM_BE (installed_size);

  g_variant_lookup (commit_metadata, OSTREE_COMMIT_META_KEY_ENDOFLIFE, "s", &op->eol);
  g_variant_lookup (commit_metadata, OSTREE_COMMIT_META_KEY_ENDOFLIFE_REBASE, "s", &op->eol_rebase);

  if (op->eol_rebase)
    {
      g_autoptr(FlatpakDecomposed) eolr_decomposed = NULL;
      eolr_decomposed = flatpak_decomposed_new_from_ref (op->eol_rebase, error);
      if (!eolr_decomposed)
        return FALSE;
      if (flatpak_decomposed_get_kind (op->ref) != flatpak_decomposed_get_kind (eolr_decomposed))
        return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA,
                                   "end-of-life-rebase on commit %s has the wrong type (%s -> %s)",
                                   checksum, flatpak_decomposed_get_ref (op->ref),
                                   flatpak_decomposed_get_ref (eolr_decomposed));
    }

  return resolve_op_end (self, op, checksum, sideload_path, metadata_bytes, error);
}

/* NOTE: In case of non-available summary this returns FALSE with a
 * NULL error, but for other error cases it will be set.
 */
static gboolean
try_resolve_op_from_metadata (FlatpakTransaction *self,
                              FlatpakTransactionOperation *op,
                              const char *checksum,
                              GFile *sideload_path,
                              FlatpakRemoteState *state,
                              GError **error)
{
  g_autoptr(GBytes) metadata_bytes = NULL;
  guint64 download_size = 0;
  guint64 installed_size = 0;
  const char *metadata = NULL;
  VarMetadataRef sparse_cache;
  VarRefInfoRef info;
  g_autofree char *summary_checksum = NULL;

  /* Ref has to match the actual commit in the summary */
  if ((state->summary == NULL && state->index == NULL) ||
      !flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (op->ref),
                                        &summary_checksum, NULL, NULL, NULL, NULL) ||
      strcmp (summary_checksum, checksum) != 0)
    return FALSE;

  /* And, we must have the actual cached data in the summary */
  if (!flatpak_remote_state_lookup_cache (state, flatpak_decomposed_get_ref (op->ref),
                                          &download_size, &installed_size, &metadata, NULL))
      return FALSE;

  metadata_bytes = g_bytes_new (metadata, strlen (metadata));

  if (flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (op->ref),
                                       NULL, NULL, &info, NULL, NULL))
    op->summary_metadata = var_metadata_dup_to_gvariant (var_ref_info_get_metadata (info));

  op->installed_size = installed_size;
  op->download_size = download_size;

  op->token_type = state->default_token_type;

  if (flatpak_remote_state_lookup_sparse_cache (state, flatpak_decomposed_get_ref (op->ref), &sparse_cache, NULL))
    {
      op->eol = g_strdup (var_metadata_lookup_string (sparse_cache, FLATPAK_SPARSE_CACHE_KEY_ENDOFLINE, NULL));
      op->eol_rebase = g_strdup (var_metadata_lookup_string (sparse_cache, FLATPAK_SPARSE_CACHE_KEY_ENDOFLINE_REBASE, NULL));
      op->token_type = GINT32_FROM_LE (var_metadata_lookup_int32 (sparse_cache, FLATPAK_SPARSE_CACHE_KEY_TOKEN_TYPE, op->token_type));

      if (op->eol_rebase)
        {
          g_autoptr(FlatpakDecomposed) eolr_decomposed = NULL;
          eolr_decomposed = flatpak_decomposed_new_from_ref (op->eol_rebase, error);
          if (!eolr_decomposed)
            return FALSE;
          if (flatpak_decomposed_get_kind (op->ref) != flatpak_decomposed_get_kind (eolr_decomposed))
            return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA,
                                       "end-of-life-rebase on commit %s has the wrong type (%s -> %s)",
                                       checksum, flatpak_decomposed_get_ref (op->ref),
                                       flatpak_decomposed_get_ref (eolr_decomposed));
        }
    }

  return resolve_op_end (self, op, checksum, sideload_path, metadata_bytes, error);
}

static gboolean
op_may_need_token (FlatpakTransactionOperation *op)
{
  return
    !op->skip &&
    !op->update_only_deploy &&
    (op->kind == FLATPAK_TRANSACTION_OPERATION_INSTALL ||
     op->kind == FLATPAK_TRANSACTION_OPERATION_UPDATE  ||
     op->kind == FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE);
}

/* Resolving an operation means figuring out the target commit
   checksum and the metadata for that commit, so that we can handle
   dependencies from it, and verify versions. */
static gboolean
resolve_ops (FlatpakTransaction *self,
             GCancellable       *cancellable,
             GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  GList *l;

  for (l = priv->ops; l != NULL; l = l->next)
    {
      FlatpakTransactionOperation *op = l->data;
      g_autoptr(FlatpakRemoteState) state = NULL;
      g_autofree char *checksum = NULL;
      g_autoptr(GBytes) metadata_bytes = NULL;

      if (op->resolved)
        continue;

      if (op->skip)
        {
          /* We're not yet resolved, but marked skip anyway, this can happen if during
           * request_required_tokens() we were normalized away even though not fully resolved.
           * For example we got the checksum but need to auth to get the commit, but the
           * checksum we got was the version already installed.
           */
          g_assert (op->resolved_commit != NULL);
          if (!mark_op_resolved (op, op->resolved_commit, NULL, NULL, NULL, error))
            return FALSE;
          continue;
        }

      if (op->kind == FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
        {
          /* We resolve to the deployed metadata, because we need it to uninstall related ops */

          metadata_bytes = load_deployed_metadata (self, op->ref, &checksum, NULL);
          if (metadata_bytes == NULL)
            {
              op->skip = TRUE;
              continue;
            }
          if (!mark_op_resolved (op, checksum, NULL, metadata_bytes, NULL, error))
            return FALSE;
          continue;
        }

      if (op->kind == FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE)
        {
          g_assert (op->commit != NULL);
          if (!mark_op_resolved (op, op->commit, NULL, op->external_metadata, NULL, error))
            return FALSE;
          continue;
        }

      /* op->kind is INSTALL or UPDATE */

      if (flatpak_decomposed_is_app (op->ref))
        {
          if (op->kind == FLATPAK_TRANSACTION_OPERATION_INSTALL)
            priv->max_op = APP_INSTALL;
          else
            priv->max_op = MAX (priv->max_op, APP_UPDATE);
        }
      else if (flatpak_decomposed_is_runtime (op->ref))
        {
          if (op->kind == FLATPAK_TRANSACTION_OPERATION_INSTALL)
            priv->max_op = MAX (priv->max_op, RUNTIME_INSTALL);
        }

      state = flatpak_transaction_ensure_remote_state (self, op->kind, op->remote, NULL, error);
      if (state == NULL)
        return FALSE;

      /* Should we use local state */
      if (transaction_is_local_only (self, op->kind))
        {
          g_autoptr(GVariant) commit_data = flatpak_dir_read_latest_commit (priv->dir, op->remote, op->ref,
                                                                            &checksum, NULL, error);
          if (commit_data == NULL)
            return FALSE;

          if (!resolve_op_from_commit (self, op, checksum, NULL, commit_data, error))
            return FALSE;
        }
      else
        {
          g_autoptr(GError) local_error = NULL;
          g_autoptr(GFile) sideload_path = NULL;

          if (op->commit != NULL)
            {
              checksum = g_strdup (op->commit);
              /* Check if this is available offline and if so, use that */
              sideload_path = flatpak_remote_state_lookup_sideload_checksum (state, op->commit);
            }
          else
            {
              g_autofree char *latest_checksum = NULL;
              g_autoptr(GFile) latest_sideload_path = NULL;
              g_autofree char *local_checksum = NULL;
              guint64 latest_timestamp;
              g_autoptr(GVariant) local_commit_data = flatpak_dir_read_latest_commit (priv->dir, op->remote, op->ref,
                                                                                      &local_checksum, NULL, NULL);

              if (flatpak_dir_find_latest_rev (priv->dir, state, flatpak_decomposed_get_ref (op->ref), op->commit,
                                               &latest_checksum, &latest_timestamp, &latest_sideload_path,
                                               cancellable, &local_error))
                {
                  /* If we found the latest in a sideload repo, it may be older that what is locally available, check timestamps.
                   * Note: If the timestamps are equal (timestamp granularity issue), assume we want to update */
                  if (latest_sideload_path != NULL && local_commit_data && latest_timestamp != 0 &&
                      ostree_commit_get_timestamp (local_commit_data) > latest_timestamp)
                    {
                      g_info ("Installed commit %s newer than sideloaded %s, ignoring", local_checksum, latest_checksum);
                      checksum = g_steal_pointer (&local_checksum);
                    }
                  else
                    {
                      /* Otherwise, use whatever we found */
                      checksum = g_steal_pointer (&latest_checksum);
                      sideload_path = g_steal_pointer (&latest_sideload_path);
                    }
                }
              else
                {
                  /* Ref not available in the remote (maybe offline), resolve to local version if installed */
                  if (local_commit_data == NULL)
                    {
                      g_propagate_error (error, g_steal_pointer (&local_error));
                      return FALSE;
                    }

                  g_message (_("Warning: Treating remote fetch error as non-fatal since %s is already installed: %s"),
                             flatpak_decomposed_get_ref (op->ref), local_error->message);
                  g_clear_error (&local_error);

                  checksum = g_steal_pointer (&local_checksum);
                }
            }

          /* First try to resolve via metadata (if remote is available and its metadata matches the commit version) */
          if (!try_resolve_op_from_metadata (self, op, checksum, sideload_path, state, &local_error))
            {
              if (local_error)
                {
                  /* Actual error, not just missing from summary */
                  g_propagate_error (error, g_steal_pointer (&local_error));
                  return FALSE;
                }

              /* Missing from summary, try to load the commit object.
               * Note, we don't have a token here, so this will not work for authenticated apps.
               * We handle this by catching the 401 http status and retrying. */
              g_autoptr(GVariant) commit_data = NULL;
              VarRefInfoRef ref_info;

              /* OCI needs this to get the oci repository for the ref to request the token, so lets always set it here */
              if (op->summary_metadata == NULL &&
                  flatpak_remote_state_lookup_ref (state, flatpak_decomposed_get_ref (op->ref),
                                                   NULL, NULL, &ref_info, NULL, NULL))
                op->summary_metadata = var_metadata_dup_to_gvariant (var_ref_info_get_metadata (ref_info));

              commit_data = flatpak_remote_state_load_ref_commit (state, priv->dir,
                                                                  flatpak_decomposed_get_ref (op->ref),
                                                                  checksum, /* initially NULL */ op->resolved_token,
                                                                  NULL, NULL, &local_error);
              if (commit_data == NULL)
                {
                  if (g_error_matches (local_error, FLATPAK_HTTP_ERROR, FLATPAK_HTTP_ERROR_UNAUTHORIZED) && !op->requested_token)
                    {

                      g_info ("Unauthorized access during resolve by commit of %s, retrying with token", flatpak_decomposed_get_ref (op->ref));
                      priv->needs_resolve = TRUE;
                      priv->needs_tokens = TRUE;

                      /* Token type maxint32 means we don't know the type */
                      op->token_type = G_MAXINT32;
                      op->resolved_commit = g_strdup (checksum);

                      g_clear_error (&local_error);
                      continue;
                    }
                  g_propagate_error (error, g_steal_pointer (&local_error));
                  return FALSE;
                }

              if (!resolve_op_from_commit (self, op, checksum, sideload_path, commit_data, error))
                return FALSE;
            }
        }
    }

  return TRUE;
}

static gboolean
resolve_all_ops (FlatpakTransaction *self,
                 GCancellable       *cancellable,
                 GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  while (priv->needs_resolve)
    {
      priv->needs_resolve = FALSE;
      priv->needs_tokens = FALSE;
      if (!resolve_ops (self, cancellable, error))
        return FALSE;

      /* We might need tokens early, if reading individual commits needs it,
       * otherwise we try to delay to bunch the requests */
      if (priv->needs_tokens)
        {
          if (!request_required_tokens (self, NULL, cancellable, error))
            return FALSE;
        }
    }

  return TRUE;
}

static void
request_tokens_response (FlatpakAuthenticatorRequest *object,
                         guint response,
                         GVariant *results,
                         RequestData *data)
{
  FlatpakTransaction *transaction = data->transaction;
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (transaction);

  if (data->done)
    return; /* Don't respond twice */

  g_assert (priv->active_request_id == 0); /* It should have reported done */

  data->response = response;
  data->results = g_variant_ref (results);
  data->done = TRUE;
  g_main_context_wakeup (g_main_context_get_thread_default ());
}

static void
request_tokens_webflow (FlatpakAuthenticatorRequest *object,
                        const gchar *arg_uri,
                        GVariant *options,
                        RequestData *data)
{
  g_autoptr(FlatpakTransaction) transaction = g_object_ref (data->transaction);
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (transaction);
  gboolean retval = FALSE;

  if (data->done)
    return; /* Don't respond twice */

  g_assert (priv->active_request_id == 0);
  priv->active_request_id = ++priv->next_request_id;

  g_info ("Webflow start %s", arg_uri);
  g_signal_emit (transaction, signals[WEBFLOW_START], 0, data->remote, arg_uri, options, priv->active_request_id, &retval);
  if (!retval)
    {
      g_autoptr(GError) local_error = NULL;

      priv->active_request_id = 0;

      /* We didn't handle the uri, cancel the auth op. */
      if (!flatpak_authenticator_request_call_close_sync (data->request, NULL, &local_error))
        g_info ("Failed to close auth request: %s", local_error->message);
    }
}

static void
request_tokens_webflow_done (FlatpakAuthenticatorRequest *object,
                             GVariant *options,
                             RequestData *data)
{
  g_autoptr(FlatpakTransaction) transaction = g_object_ref (data->transaction);
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (transaction);
  guint id;

  if (data->done)
    return; /* Don't respond twice */

  g_assert (priv->active_request_id != 0);
  id = priv->active_request_id;
  priv->active_request_id = 0;

  g_info ("Webflow done");
  g_signal_emit (transaction, signals[WEBFLOW_DONE], 0, options, id);
}

static void
request_tokens_basic_auth (FlatpakAuthenticatorRequest *object,
                           const gchar *arg_realm,
                           GVariant *options,
                           RequestData *data)
{
  g_autoptr(FlatpakTransaction) transaction = g_object_ref (data->transaction);
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (transaction);
  gboolean retval = FALSE;

  if (data->done)
    return; /* Don't respond twice */

  g_assert (priv->active_request_id == 0);
  priv->active_request_id = ++priv->next_request_id;

  g_info ("BasicAuth start %s", arg_realm);
  g_signal_emit (transaction, signals[BASIC_AUTH_START], 0, data->remote, arg_realm, options, priv->active_request_id, &retval);
  if (!retval)
    {
      g_autoptr(GError) local_error = NULL;

      priv->active_request_id = 0;

      /* We didn't handle the request, cancel the auth op. */
      if (!flatpak_authenticator_request_call_close_sync (data->request, NULL, &local_error))
        g_info ("Failed to close auth request: %s", local_error->message);
    }

}

/**
 * flatpak_transaction_abort_webflow:
 * @self: a #FlatpakTransaction
 * @id: The webflow id, as passed into the webflow-start signal
 *
 * Cancel an ongoing webflow authentication request. This can be call
 * in the time between #FlatpakTransaction::webflow-start returned
 * %TRUE, and #FlatpakTransaction::webflow-done is emitted. It will
 * cancel the ongoing authentication operation.
 *
 * This is useful for example if you're showing an authenticaion
 * window with a browser, but the user closed it before it was finished.
 *
 * Since: 1.5.1
 */
void
flatpak_transaction_abort_webflow (FlatpakTransaction *self,
                                   guint id)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(GError) local_error = NULL;

  if (priv->active_request_id == id)
    {
      RequestData *data = priv->active_request;

      g_assert (data != NULL);
      priv->active_request_id = 0;

      if (!data->done)
        {
          if (!flatpak_authenticator_request_call_close_sync (data->request, NULL, &local_error))
            g_info ("Failed to close auth request: %s", local_error->message);
        }
    }
}

/**
 * flatpak_transaction_complete_basic_auth:
 * @self: a #FlatpakTransaction
 * @id: The webflow id, as passed into the webflow-start signal
 * @user: The user name, or %NULL if aborting request
 * @password: The password
 * @options: Extra a{sv] variant with options (or %NULL), currently unused.
 *
 * Finishes (or aborts) an ongoing basic auth request.
 *
 * Since: 1.5.2
 */
void
flatpak_transaction_complete_basic_auth (FlatpakTransaction *self,
                                         guint id,
                                         const char *user,
                                         const char *password,
                                         GVariant *options)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(GError) local_error = NULL;
  g_autoptr(GVariant) default_options = NULL;

  if (options == NULL)
    {
      default_options = g_variant_ref_sink (g_variant_new_array (G_VARIANT_TYPE ("{sv}"), NULL, 0));
      options = default_options;
    }

  if (priv->active_request_id == id)
    {
      RequestData *data = priv->active_request;

      g_assert (data != NULL);
      priv->active_request_id = 0;

      if (user == NULL)
        {
          if (!flatpak_authenticator_request_call_close_sync (data->request, NULL, &local_error))
            g_info ("Failed to abort basic auth request: %s", local_error->message);
        }
      else
        {
          if (!flatpak_authenticator_request_call_basic_auth_reply_sync (data->request,
                                                                         user, password,
                                                                         options,
                                                                         NULL, &local_error))
            g_info ("Failed to reply to basic auth request: %s", local_error->message);
        }
    }
}

static void
copy_summary_data (GVariantBuilder *builder, GVariant *summary, const char *key)
{
  g_autoptr(GVariant) extensions = g_variant_get_child_value (summary, 1);
  g_autoptr(GVariant) value = NULL;

  value = g_variant_lookup_value (extensions, key, NULL);
  if (value)
    g_variant_builder_add (builder, "{s@v}", key, g_variant_new_variant (value));
}


static gboolean
request_tokens_for_remote (FlatpakTransaction *self,
                           const char         *remote,
                           GList              *ops,
                           GCancellable       *cancellable,
                           GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(GString) refs_as_str = g_string_new ("");
  GList *l;
  g_autoptr(AutoFlatpakAuthenticatorRequest) request = NULL;
  g_autoptr(AutoFlatpakAuthenticator) authenticator = NULL;
  g_autoptr(GMainContextPopDefault) context = NULL;
  RequestData data = { self, remote };
  g_autoptr(GVariant) tokens = NULL;
  g_autoptr(GVariant) results = NULL;
  g_autoptr(GVariant) refs = NULL;
  GVariantBuilder refs_builder;
  g_autofree char *remote_url = NULL;
  g_autoptr(GVariantBuilder) extra_builder = NULL;
  FlatpakRemoteState *state;
  g_autoptr(FlatpakDecomposed) auto_install_ref = NULL;

  auto_install_ref = flatpak_dir_get_remote_auto_install_authenticator_ref (priv->dir, remote);
  if (auto_install_ref != NULL)
    {
      g_autoptr(GFile) deploy = NULL;
      deploy = flatpak_dir_get_if_deployed (priv->dir, auto_install_ref, NULL, cancellable);
      if (deploy == NULL)
        g_signal_emit (self, signals[INSTALL_AUTHENTICATOR], 0,
                       remote, flatpak_decomposed_get_ref (auto_install_ref));
      deploy = flatpak_dir_get_if_deployed (priv->dir, auto_install_ref, NULL, cancellable);
      if (deploy == NULL)
        return flatpak_fail (error, _("No authenticator installed for remote '%s'"), remote);
    }

  if (!ostree_repo_remote_get_url (flatpak_dir_get_repo (priv->dir), remote, &remote_url, error))
    return FALSE;

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

  for (l = ops; l != NULL; l = l->next)
    {
      FlatpakTransactionOperation *op = l->data;
      g_autoptr(GVariantBuilder) metadata_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));

      if (op->summary_metadata)
        {
          const int n = g_variant_n_children (op->summary_metadata);
          for (int i = 0; i < n; i++)
            {
              const char *key;
              g_autofree char *new_key = NULL;
              g_autoptr(GVariant) value = NULL;

              g_variant_get_child (op->summary_metadata, i, "{&s@v}", &key, &value);

              new_key = g_strconcat ("summary.", key, NULL);
              g_variant_builder_add (metadata_builder, "{s@v}", new_key, value);
            }
        }

      g_variant_builder_add (&refs_builder, "(ssi@a{sv})", flatpak_decomposed_get_ref (op->ref),
                             op->resolved_commit ? op->resolved_commit : "", (gint32)op->token_type, g_variant_builder_end (metadata_builder));
      g_string_append_printf (refs_as_str, "(%s, %s %d)", flatpak_decomposed_get_ref (op->ref),
                              op->resolved_commit ? op->resolved_commit : "", op->token_type);
      if (l->next != NULL)
        g_string_append (refs_as_str, ", ");
    }

  g_info ("Requesting tokens for remote %s: %s", remote, refs_as_str->str);
  refs = g_variant_ref_sink (g_variant_builder_end (&refs_builder));

  extra_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));

  state = g_hash_table_lookup (priv->remote_states, remote);
  if (state && state->summary)
    {
      copy_summary_data (extra_builder, state->summary, "xa.oci-registry-uri");
    }

  if (flatpak_dir_get_no_interaction (priv->dir))
    g_variant_builder_add (extra_builder, "{sv}", "no-interaction", g_variant_new_boolean (TRUE));

  context = flatpak_main_context_new_default ();

  authenticator = flatpak_auth_new_for_remote (priv->dir, remote, cancellable, error);
  if (authenticator == NULL)
    return FALSE;

  request = flatpak_auth_create_request (authenticator, cancellable, error);
  if (request == NULL)
    return FALSE;

  g_signal_connect (request, "webflow", (GCallback)request_tokens_webflow, &data);
  g_signal_connect (request, "webflow-done", (GCallback)request_tokens_webflow_done, &data);
  g_signal_connect (request, "response", (GCallback)request_tokens_response, &data);
  g_signal_connect (request, "basic-auth", (GCallback)request_tokens_basic_auth, &data);

  priv->active_request = &data;

  data.request = request;
  if (!flatpak_auth_request_ref_tokens (authenticator, request, remote, remote_url, refs, g_variant_builder_end (extra_builder),
                                        priv->parent_window, cancellable, error))
    return FALSE;

  while (!data.done)
    g_main_context_iteration (context, TRUE);

  g_assert (priv->active_request_id == 0); /* No outstanding requests */
  priv->active_request = NULL;

  results = data.results; /* Make sure its freed as needed */

  {
    g_autofree char *results_str = results != NULL ? g_variant_print (results, FALSE) : g_strdup ("NULL");
    g_info ("Response from request_tokens: %d - %s\n", data.response, results_str);
  }

  if (data.response == FLATPAK_AUTH_RESPONSE_CANCELLED)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED,
                   "User cancelled authentication request");
      return FALSE;
    }

  if (data.response != FLATPAK_AUTH_RESPONSE_OK)
    {
      const char *error_message;
      gint32 error_code;

      if (!g_variant_lookup (results, "error-message", "&s", &error_message))
        error_message = NULL;

      if (g_variant_lookup (results, "error-code", "i", &error_code) && error_code != -1)
        {
          if (error_message)
            return flatpak_fail_error (error, error_code, _("Failed to get tokens for ref: %s"), error_message);
          else
            return flatpak_fail_error (error, error_code, _("Failed to get tokens for ref"));
        }
      else
        {
          if (error_message)
            return flatpak_fail (error, _("Failed to get tokens for ref: %s"), error_message);
          else
            return flatpak_fail (error, _("Failed to get tokens for ref"));
        }
    }

  tokens = g_variant_lookup_value (results, "tokens", G_VARIANT_TYPE ("a{sas}"));
  if (tokens == NULL)
    return flatpak_fail (error, "Authenticator didn't send requested tokens");

  for (l = ops; l != NULL; l = l->next)
    {
      FlatpakTransactionOperation *op = l->data;
      GVariantIter iter;
      const char *token = NULL;
      const char *token_for_refs;
      g_autofree const char **refs_strv;

      g_variant_iter_init (&iter, tokens);
      while (g_variant_iter_next (&iter, "{&s^a&s}", &token_for_refs, &refs_strv))
        {
          if (g_strv_contains (refs_strv, flatpak_decomposed_get_ref (op->ref)))
            {
              token = token_for_refs;
              break;
            }
        }

      if (token == NULL)
        return flatpak_fail (error, "Authenticator didn't send tokens for ref");

      /* Allow sending empty tokens to mean no token needed */

      op->resolved_token = *token == 0 ? NULL : g_strdup (token);
      op->requested_token = TRUE;
    }

  return TRUE;
}

static gboolean
request_required_tokens (FlatpakTransaction *self,
                         const char         *optional_remote, /* else all remotes */
                         GCancellable       *cancellable,
                         GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  GList *l;
  g_autoptr(GHashTable) need_token_ht = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify) g_list_free); /* remote name -> list of op */

  /* Ensure all ops so far ar normalized so we don't request authentication for no-op updates */
  flatpak_transaction_normalize_ops (self);

  for (l = priv->ops; l != NULL; l = l->next)
    {
      FlatpakTransactionOperation *op = l->data;
      GList *old;

      if (!flatpak_transaction_operation_get_requires_authentication (op))
        continue;

      if (optional_remote != NULL && g_strcmp0 (op->remote, optional_remote) != 0)
        continue;

      old = g_hash_table_lookup (need_token_ht, op->remote);
      if (old == NULL)
        g_hash_table_insert (need_token_ht, op->remote, g_list_append (NULL, op));
      else
        old = g_list_append (old, op);
    }

  GLNX_HASH_TABLE_FOREACH_KV(need_token_ht, const char *, remote, GList *, remote_ops)
    {
      if (!request_tokens_for_remote (self, remote, remote_ops, cancellable, error))
        return FALSE;
    }

  return TRUE;
}

static int
compare_op_ref (FlatpakTransactionOperation *a, FlatpakTransactionOperation *b)
{
  const char *aa = flatpak_decomposed_get_pref (a->ref);
  const char *bb = flatpak_decomposed_get_pref (b->ref);

  if (a->run_last != b->run_last)
    {
      if (a->run_last)
        return 1;
      return -1;
    }

  return g_strcmp0 (aa, bb);
}

static int
compare_op_prio (FlatpakTransactionOperation *a, FlatpakTransactionOperation *b)
{
  return b->run_after_prio - a->run_after_prio;
}

static void
sort_ops (FlatpakTransaction *self)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  GList *sorted = NULL;
  GList *remaining;
  GList *runnable = NULL;
  GList *l, *next;

  remaining = priv->ops;
  priv->ops = NULL;

  /* First mark runnable all jobs that depend on nothing.
     Note that this essentially reverses the original list, so these
     are in the same order as specified */
  for (l = remaining; l != NULL; l = next)
    {
      FlatpakTransactionOperation *op = l->data;
      next = l->next;

      if (op->run_after_count == 0)
        {
          remaining = g_list_remove_link (remaining, l);
          runnable = g_list_concat (l, runnable);
        }
    }

  /* If no other order, start in alphabetical ref-order */
  runnable = g_list_sort (runnable, (GCompareFunc) compare_op_ref);

  while (runnable)
    {
      GList *run = runnable;
      FlatpakTransactionOperation *run_op = run->data;

      /* Put the first runnable on the sorted list */
      runnable = g_list_remove_link (runnable, run);
      sorted = g_list_concat (run, sorted); /* prepends, so reverse at the end */

      /* Then greedily run ops that become runnable, in run_after_prio order, so that
         related ops are run before dependencies */
      run_op->run_before_ops = g_list_sort (run_op->run_before_ops, (GCompareFunc) compare_op_prio);
      for (l = run_op->run_before_ops; l != NULL; l = l->next)
        {
          FlatpakTransactionOperation *after_op = l->data;
          after_op->run_after_count--;
          if (after_op->run_after_count == 0)
            {
              GList *after_l = g_list_find (remaining, after_op);
              g_assert (after_l != NULL);
              remaining = g_list_remove_link (remaining, after_l);
              runnable = g_list_concat (after_l, runnable);
            }
        }
    }

  if (remaining != NULL)
    {
      g_warning ("ops remaining after sort, maybe there is a dependency loop?");
      sorted = g_list_concat (remaining, sorted);
    }

  priv->ops = g_list_reverse (sorted);
}

/**
 * flatpak_transaction_get_operations:
 * @self: a #FlatpakTransaction
 *
 * Gets the list of operations. Skipped operations are not included. The order
 * of the list is the order in which the operations are executed.
 *
 * Returns: (transfer full) (element-type FlatpakTransactionOperation): a #GList of operations
 */
GList *
flatpak_transaction_get_operations (FlatpakTransaction *self)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  GList *l;
  GList *non_skipped = NULL;

  non_skipped = NULL;
  for (l = priv->ops; l != NULL; l = l->next)
    {
      FlatpakTransactionOperation *op = l->data;
      if (!op->skip)
        non_skipped = g_list_prepend (non_skipped, g_object_ref (op));
    }
  return g_list_reverse (non_skipped);
}

/**
 * flatpak_transaction_get_current_operation:
 * @self: a #FlatpakTransaction
 *
 * Gets the current operation.
 *
 * Returns: (transfer full): the current #FlatpakTransactionOperation
 */
FlatpakTransactionOperation *
flatpak_transaction_get_current_operation (FlatpakTransaction *self)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  if (priv->current_op)
    return g_object_ref (priv->current_op);

  return NULL;
}

/**
 * flatpak_transaction_get_operation_for_ref:
 * @self: a #FlatpakTransaction
 * @remote: (nullable): a remote name
 * @ref: a ref
 * @error: return location for an error
 *
 * Gets the operation for @ref, if any match. If @remote is non-%NULL, only an
 * operation for that remote will be returned. If remote is %NULL and the
 * transaction has more than one operation for @ref from different remotes, an
 * error will be returned.
 *
 * Returns: (transfer full): the #FlatpakTransactionOperation for @ref, or
 *   %NULL with @error set
 * Since: 1.13.3
 */
FlatpakTransactionOperation *
flatpak_transaction_get_operation_for_ref (FlatpakTransaction *self,
                                           const char         *remote,
                                           const char         *ref,
                                           GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(FlatpakDecomposed) decomposed_ref = NULL;
  g_autoptr(FlatpakTransactionOperation) matching_op = NULL;
  GList *l;

  g_return_val_if_fail (ref != NULL, NULL);

  decomposed_ref = flatpak_decomposed_new_from_ref (ref, error);
  if (decomposed_ref == NULL)
    return NULL;

  for (l = priv->ops; l != NULL; l = l->next)
    {
      FlatpakTransactionOperation *op = l->data;

      if (remote != NULL && g_strcmp0 (remote, op->remote) != 0)
        continue;

      if (flatpak_decomposed_equal (op->ref, decomposed_ref))
        {
          if (matching_op == NULL)
            matching_op = g_object_ref (op);
          else
            {
              flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA,
                                  _("Ref %s from %s matches more than one transaction operation"),
                                  ref, remote ? remote : _("any remote"));
              return NULL;
            }
        }
    }

  if (matching_op == NULL)
    {
      flatpak_fail_error (error, FLATPAK_ERROR_REF_NOT_FOUND,
                          _("No transaction operation found for ref %s from %s"),
                          ref, remote ? remote : _("any remote"));
      return NULL;
    }

  return g_steal_pointer (&matching_op);
}

/**
 * flatpak_transaction_get_installation:
 * @self: a #FlatpakTransactionOperation
 *
 * Gets the installation this transaction was created for.
 *
 * Returns: (transfer full): a #FlatpakInstallation
 */
FlatpakInstallation *
flatpak_transaction_get_installation (FlatpakTransaction *self)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);

  return g_object_ref (priv->installation);
}

static gboolean
remote_is_already_configured (FlatpakTransaction *self,
                              const char         *url)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autofree char *old_remote = NULL;

  old_remote = flatpak_dir_find_remote_by_uri (priv->dir, url);

  /* Note: we don't check priv->extra_dependency_dirs because the transaction
   * can only operate on one installation so any install/update ops need to
   * have a remote there. */

  return old_remote != NULL;
}

static gboolean
handle_suggested_remote_name (FlatpakTransaction *self,
                              GKeyFile *keyfile,
                              GKeyFile *runtime_repo_keyfile, /* nullable */
                              GError **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autofree char *suggested_name = NULL;
  g_autofree char *name = NULL;
  g_autofree char *url = NULL;
  g_autoptr(GKeyFile) config = NULL;
  g_autoptr(GBytes) gpg_key = NULL;
  gboolean res;

  suggested_name = g_key_file_get_string (keyfile, FLATPAK_REF_GROUP,
                                          FLATPAK_REF_SUGGEST_REMOTE_NAME_KEY, NULL);
  if (suggested_name == NULL)
    return TRUE;

  name = g_key_file_get_string (keyfile, FLATPAK_REF_GROUP, FLATPAK_REF_NAME_KEY, NULL);
  if (name == NULL)
    return TRUE;

  url = g_key_file_get_string (keyfile, FLATPAK_REF_GROUP, FLATPAK_REF_URL_KEY, NULL);
  if (url == NULL)
    return TRUE;

  if (remote_is_already_configured (self, url))
    return TRUE;

  /* The name is already used, ignore */
  if (ostree_repo_remote_get_url (flatpak_dir_get_repo (priv->dir), suggested_name, NULL, NULL))
    return TRUE;

  res = FALSE;
  g_signal_emit (self, signals[ADD_NEW_REMOTE], 0, FLATPAK_TRANSACTION_REMOTE_GENERIC_REPO,
                 name, suggested_name, url, &res);
  if (res)
    {
      g_autofree char *runtime_repo_url = NULL;

      /* In case the runtime repo is the same repo, use its title, comment,
       * description, etc. since flatpakref files don't have those fields. */
      runtime_repo_url = g_key_file_get_string (runtime_repo_keyfile, FLATPAK_REPO_GROUP, FLATPAK_REPO_URL_KEY, NULL);
      if (runtime_repo_url != NULL && flatpak_uri_equal (runtime_repo_url, url))
        config = flatpak_parse_repofile (suggested_name, FALSE, runtime_repo_keyfile, &gpg_key, NULL, error);
      else
        config = flatpak_parse_repofile (suggested_name, TRUE, keyfile, &gpg_key, NULL, error);

      if (config == NULL)
        return FALSE;

      if (!flatpak_dir_modify_remote (priv->dir, suggested_name, config, gpg_key, NULL, error))
        return FALSE;

      if (!flatpak_dir_recreate_repo (priv->dir, NULL, error))
        return FALSE;

      flatpak_installation_drop_caches (priv->installation, NULL, NULL);
    }

  return TRUE;
}

static gboolean
load_flatpakrepo_file (FlatpakTransaction *self,
                       const char         *dep_url,
                       GKeyFile          **out_keyfile,
                       GCancellable       *cancellable,
                       GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(GBytes) dep_data = NULL;
  g_autoptr(GKeyFile) dep_keyfile = g_key_file_new ();
  g_autoptr(GError) local_error = NULL;
  g_autoptr(FlatpakHttpSession) http_session = NULL;

  if (priv->disable_deps)
    return TRUE;

  if (!g_str_has_prefix (dep_url, "http:") &&
      !g_str_has_prefix (dep_url, "https:") &&
      !g_str_has_prefix (dep_url, "file:"))
    return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Flatpakrepo URL %s not file, HTTP or HTTPS"), dep_url);

  http_session = flatpak_create_http_session (PACKAGE_STRING);
  dep_data = flatpak_load_uri (http_session, dep_url, 0, NULL, NULL, NULL, NULL, cancellable, error);
  if (dep_data == NULL)
    {
      g_prefix_error (error, _("Can't load dependent file %s: "), dep_url);
      return FALSE;
    }

  if (!g_key_file_load_from_data (dep_keyfile,
                                  g_bytes_get_data (dep_data, NULL),
                                  g_bytes_get_size (dep_data),
                                  0, &local_error))
    return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Invalid .flatpakrepo: %s"), local_error->message);

  if (out_keyfile)
    *out_keyfile = g_steal_pointer (&dep_keyfile);

  return TRUE;
}

static gboolean
handle_runtime_repo_deps (FlatpakTransaction *self,
                          const char         *id,
                          const char         *dep_url,
                          GKeyFile           *dep_keyfile,
                          GCancellable       *cancellable,
                          GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autofree char *runtime_url = NULL;
  g_autofree char *new_remote = NULL;
  g_autofree char *basename = NULL;
  g_autoptr(GUri) uri = NULL;
  g_auto(GStrv) remotes = NULL;
  g_autoptr(GKeyFile) config = NULL;
  g_autoptr(GBytes) gpg_key = NULL;
  g_autofree char *group = NULL;
  char *t;
  int i;
  gboolean res;

  if (priv->disable_deps)
    return TRUE;

  g_assert (dep_keyfile != NULL);

  uri = g_uri_parse (dep_url, FLATPAK_HTTP_URI_FLAGS | G_URI_FLAGS_PARSE_RELAXED, NULL);
  basename = g_path_get_basename (g_uri_get_path (uri));
  /* Strip suffix */
  t = strchr (basename, '.');
  if (t != NULL)
    *t = 0;

  /* Find a free remote name */
  remotes = flatpak_dir_list_remotes (priv->dir, NULL, NULL);
  i = 0;
  do
    {
      g_clear_pointer (&new_remote, g_free);

      if (i == 0)
        new_remote = g_strdup (basename);
      else
        new_remote = g_strdup_printf ("%s-%d", basename, i);
      i++;
    }
  while (remotes != NULL && g_strv_contains ((const char * const *) remotes, new_remote));

  config = flatpak_parse_repofile (new_remote, FALSE, dep_keyfile, &gpg_key, NULL, error);
  if (config == NULL)
    {
      g_prefix_error (error, "Can't parse dependent file %s: ", dep_url);
      return FALSE;
    }

  /* See if it already exists */
  group = g_strdup_printf ("remote \"%s\"", new_remote);
  runtime_url = g_key_file_get_string (config, group, "url", NULL);
  g_assert (runtime_url != NULL);

  if (remote_is_already_configured (self, runtime_url))
    return TRUE;

  res = FALSE;
  g_signal_emit (self, signals[ADD_NEW_REMOTE], 0, FLATPAK_TRANSACTION_REMOTE_RUNTIME_DEPS,
                 id, new_remote, runtime_url, &res);
  if (res)
    {
      if (!flatpak_dir_modify_remote (priv->dir, new_remote, config, gpg_key, NULL, error))
        return FALSE;

      if (!flatpak_dir_recreate_repo (priv->dir, NULL, error))
        return FALSE;

      flatpak_installation_drop_caches (priv->installation, NULL, NULL);
    }

  return TRUE;
}

static gboolean
handle_runtime_repo_deps_from_keyfile (FlatpakTransaction *self,
                                       GKeyFile           *flatpakref_keyfile,
                                       const char         *runtime_repo_url,
                                       GKeyFile           *runtime_repo_keyfile,
                                       GCancellable       *cancellable,
                                       GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autofree char *name = NULL;

  if (priv->disable_deps)
    return TRUE;

  name = g_key_file_get_string (flatpakref_keyfile, FLATPAK_REF_GROUP, FLATPAK_REF_NAME_KEY, NULL);
  if (name == NULL)
    return TRUE;

  return handle_runtime_repo_deps (self, name, runtime_repo_url, runtime_repo_keyfile, cancellable, error);
}

static gboolean
flatpak_transaction_resolve_flatpakrefs (FlatpakTransaction *self,
                                         GCancellable       *cancellable,
                                         GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  GList *l;

  for (l = priv->flatpakrefs; l != NULL; l = l->next)
    {
      GKeyFile *flatpakref = l->data;
      g_autofree char *remote = NULL;
      g_autofree char *runtime_repo_url = NULL;
      g_autoptr(FlatpakDecomposed) ref = NULL;
      g_autoptr(GKeyFile) runtime_repo_keyfile = NULL;

      if (!priv->disable_deps)
        {
          runtime_repo_url = g_key_file_get_string (flatpakref, FLATPAK_REF_GROUP,
                                                    FLATPAK_REF_RUNTIME_REPO_KEY, NULL);
          if (runtime_repo_url == NULL)
            g_warning ("Flatpakref file does not contain a %s", FLATPAK_REF_RUNTIME_REPO_KEY);
          else if (!load_flatpakrepo_file (self, runtime_repo_url, &runtime_repo_keyfile, cancellable, error))
            return FALSE;
        }

      /* Handle SuggestRemoteName before the runtime deps, because they might
       * be the same. Pass in the RuntimeRepo keyfile so its metadata can be
       * used in that case. */
      if (!handle_suggested_remote_name (self, flatpakref, runtime_repo_keyfile, error))
        return FALSE;

      if (runtime_repo_keyfile != NULL &&
          !handle_runtime_repo_deps_from_keyfile (self, flatpakref,
                                                  runtime_repo_url, runtime_repo_keyfile,
                                                  cancellable, error))
        return FALSE;

      if (!flatpak_dir_create_remote_for_ref_file (priv->dir, flatpakref, priv->default_arch,
                                                   &remote, NULL, &ref, error))
        return FALSE;

      /* Need to pick up the new config, in case it was applied in the system helper. */
      if (!flatpak_dir_recreate_repo (priv->dir, NULL, error))
        return FALSE;

      flatpak_installation_drop_caches (priv->installation, NULL, NULL);

      if (!flatpak_transaction_add_install (self, remote, flatpak_decomposed_get_ref (ref), NULL, error))
        return FALSE;
    }

  return TRUE;
}

static gboolean
handle_runtime_repo_deps_from_bundle (FlatpakTransaction *self,
                                      GFile              *file,
                                      GCancellable       *cancellable,
                                      GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autofree char *dep_url = NULL;
  g_autoptr(FlatpakDecomposed) ref = NULL;
  g_autoptr(GVariant) metadata = NULL;
  g_autoptr(GKeyFile) runtime_repo_keyfile = NULL;
  g_autofree char *id = NULL;

  if (priv->disable_deps)
    return TRUE;

  metadata = flatpak_bundle_load (file,
                                  NULL,
                                  &ref,
                                  NULL,
                                  &dep_url,
                                  NULL,
                                  NULL,
                                  NULL,
                                  NULL,
                                  NULL);

  if (metadata == NULL || dep_url == NULL || ref == NULL)
    return TRUE;

  id = flatpak_decomposed_dup_id (ref);

  if (!load_flatpakrepo_file (self, dep_url, &runtime_repo_keyfile, cancellable, error))
    return FALSE;

  return handle_runtime_repo_deps (self, id, dep_url, runtime_repo_keyfile, cancellable, error);
}

static gboolean
flatpak_transaction_resolve_bundles (FlatpakTransaction *self,
                                     GCancellable       *cancellable,
                                     GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  GList *l;

  for (l = priv->bundles; l != NULL; l = l->next)
    {
      BundleData *data = l->data;
      g_autofree char *remote = NULL;
      g_autofree char *commit = NULL;
      g_autofree char *metadata = NULL;
      g_autoptr(FlatpakDecomposed) ref = NULL;
      gboolean created_remote;

      if (!handle_runtime_repo_deps_from_bundle (self, data->file, cancellable, error))
        return FALSE;

      if (!flatpak_dir_ensure_repo (priv->dir, cancellable, error))
        return FALSE;

      remote = flatpak_dir_ensure_bundle_remote (priv->dir, data->file, data->gpg_data,
                                                 &ref, &commit, &metadata, &created_remote,
                                                 NULL, error);
      if (remote == NULL)
        return FALSE;

      if (created_remote)
        flatpak_installation_drop_caches (priv->installation, NULL, NULL);

      if (!flatpak_transaction_add_ref (self, remote, ref, NULL, NULL, commit,
                                        FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE,
                                        data->file, metadata, FALSE, NULL, error))
        return FALSE;
    }

  return TRUE;
}

/**
 * flatpak_transaction_run:
 * @transaction: a #FlatpakTransaction
 * @cancellable: (nullable): a #GCancellable
 * @error: return location for an error
 *
 * Executes the transaction.
 *
 * During the course of the execution, various signals will get emitted.
 * The FlatpakTransaction::choose-remote-for-ref  and
 * #FlatpakTransaction::add-new-remote signals may get emitted while
 * resolving operations. #FlatpakTransaction::ready is emitted when
 * the transaction has been fully resolved, and #FlatpakTransaction::new-operation
 * and #FlatpakTransaction::operation-done are emitted while the operations
 * are carried out. If an error occurs at any point during the execution,
 * #FlatpakTransaction::operation-error is emitted.
 *
 * Note that this call blocks until the transaction is done.
 *
 * Returns: %TRUE on success, %FALSE if an error occurred
 */
gboolean
flatpak_transaction_run (FlatpakTransaction *transaction,
                         GCancellable       *cancellable,
                         GError            **error)
{
  return FLATPAK_TRANSACTION_GET_CLASS (transaction)->run (transaction, cancellable, error);
}

static gboolean
_run_op_kind (FlatpakTransaction           *self,
              FlatpakTransactionOperation  *op,
              FlatpakRemoteState           *remote_state, /* nullable */
              gboolean                     *out_needs_prune,
              gboolean                     *out_needs_triggers,
              gboolean                     *out_needs_cache_drop,
              GCancellable                 *cancellable,
              GError                      **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  gboolean res = TRUE;

  g_return_val_if_fail (remote_state != NULL || op->kind == FLATPAK_TRANSACTION_OPERATION_UNINSTALL, FALSE);

  if (op->kind == FLATPAK_TRANSACTION_OPERATION_INSTALL)
    {
      g_autoptr(FlatpakTransactionProgress) progress = flatpak_transaction_progress_new ();
      FlatpakTransactionResult result_details = 0;
      g_autoptr(GError) local_error = NULL;

      emit_new_op (self, op, progress);

      g_assert (op->resolved_commit != NULL); /* We resolved this before */

      if (op->resolved_metakey && !flatpak_check_required_version (flatpak_decomposed_get_ref (op->ref),
                                                                   op->resolved_metakey, &local_error))
        res = FALSE;
      else
        res = flatpak_dir_install (priv->dir,
                                   priv->no_pull,
                                   priv->no_deploy,
                                   priv->disable_static_deltas,
                                   priv->reinstall,
                                   priv->max_op >= APP_UPDATE,
                                   op->pin_on_deploy,
                                   remote_state, op->ref,
                                   op->resolved_commit,
                                   (const char **) op->subpaths,
                                   (const char **) op->previous_ids,
                                   op->resolved_sideload_path,
                                   op->resolved_metadata,
                                   op->resolved_token,
                                   progress->progress_obj,
                                   cancellable, &local_error);

      flatpak_transaction_progress_done (progress);

      /* Handle noop-installs (maybe we raced, or this was installed in install-authenticator)
       * We do initial checks and fail with already installed in add_ref() for other cases. */
      if (!res && g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_ALREADY_INSTALLED))
        {
          res = TRUE;
          g_clear_error (&local_error);

          result_details |= FLATPAK_TRANSACTION_RESULT_NO_CHANGE;
        }
      else if (!res)
        {
          g_propagate_error (error, g_steal_pointer (&local_error));
        }

      if (res)
        {
          emit_op_done (self, op, result_details);

          /* Normally we don't need to prune after install, because it makes no old objects
             stale. However if we reinstall, that is not true. */
          if (!priv->no_pull && priv->reinstall)
            *out_needs_prune = TRUE;

          if (flatpak_decomposed_is_app (op->ref))
            *out_needs_triggers = TRUE;

          if (op->pin_on_deploy)
            *out_needs_cache_drop = TRUE;
        }
    }
  else if (op->kind == FLATPAK_TRANSACTION_OPERATION_UPDATE)
    {
      g_assert (op->resolved_commit != NULL); /* We resolved this before */

      if (flatpak_dir_needs_update_for_commit_and_subpaths (priv->dir, op->remote, op->ref,
                                                            op->resolved_commit, (const char **) op->subpaths))
        {
          g_autoptr(FlatpakTransactionProgress) progress = flatpak_transaction_progress_new ();
          FlatpakTransactionResult result_details = 0;
          g_autoptr(GError) local_error = NULL;

          emit_new_op (self, op, progress);

          if (op->resolved_metakey && !flatpak_check_required_version (flatpak_decomposed_get_ref (op->ref),
                                                                       op->resolved_metakey, &local_error))
            res = FALSE;
          else if (op->update_only_deploy)
            res = flatpak_dir_deploy_update (priv->dir, op->ref,
                                             op->resolved_commit,
                                             (const char **) op->subpaths,
                                             (const char **) op->previous_ids,
                                             cancellable, &local_error);
          else
            res = flatpak_dir_update (priv->dir,
                                      priv->no_pull,
                                      priv->no_deploy,
                                      priv->disable_static_deltas,
                                      op->commit != NULL, /* Allow downgrade if we specify commit */
                                      priv->max_op >= APP_UPDATE,
                                      priv->max_op == APP_INSTALL || priv->max_op == RUNTIME_INSTALL,
                                      remote_state,
                                      op->ref,
                                      op->resolved_commit,
                                      (const char **) op->subpaths,
                                      (const char **) op->previous_ids,
                                      op->resolved_sideload_path,
                                      op->resolved_metadata,
                                      op->resolved_token,
                                      progress->progress_obj,
                                      cancellable, &local_error);
          flatpak_transaction_progress_done (progress);

          /* Handle noop-updates */
          if (!res && g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_ALREADY_INSTALLED))
            {
              res = TRUE;
              g_clear_error (&local_error);

              result_details |= FLATPAK_TRANSACTION_RESULT_NO_CHANGE;
            }
          else if (!res)
            {
              g_propagate_error (error, g_steal_pointer (&local_error));
            }

          if (res)
            {
              emit_op_done (self, op, result_details);

              if (!priv->no_pull)
                *out_needs_prune = TRUE;

              if (flatpak_decomposed_is_app (op->ref))
                *out_needs_triggers = TRUE;
            }
        }
      else
        g_info ("%s need no update", flatpak_decomposed_get_ref (op->ref));
    }
  else if (op->kind == FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE)
    {
      g_autoptr(FlatpakTransactionProgress) progress = flatpak_transaction_progress_new ();
      emit_new_op (self, op, progress);
      if (op->resolved_metakey && !flatpak_check_required_version (flatpak_decomposed_get_ref (op->ref),
                                                                   op->resolved_metakey, error))
        res = FALSE;
      else
        res = flatpak_dir_install_bundle (priv->dir, op->bundle,
                                          op->remote, NULL,
                                          cancellable, error);
      flatpak_transaction_progress_done (progress);

      if (res)
        {
          emit_op_done (self, op, 0);
          *out_needs_prune = TRUE;
          *out_needs_triggers = TRUE;
        }
    }
  else if (op->kind == FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
    {
      g_autoptr(FlatpakTransactionProgress) progress = flatpak_transaction_progress_new ();
      FlatpakHelperUninstallFlags flags = 0;

      if (priv->disable_prune)
        flags |= FLATPAK_HELPER_UNINSTALL_FLAGS_KEEP_REF;

      if (priv->force_uninstall)
        flags |= FLATPAK_HELPER_UNINSTALL_FLAGS_FORCE_REMOVE;

      emit_new_op (self, op, progress);

      res = flatpak_dir_uninstall (priv->dir, op->ref, flags,
                                   cancellable, error);

      flatpak_transaction_progress_done (progress);

      if (res)
        {
          emit_op_done (self, op, 0);
          *out_needs_prune = TRUE;

          if (flatpak_decomposed_is_app (op->ref))
            *out_needs_triggers = TRUE;
        }
    }
  else
    g_assert_not_reached ();

  return res;
}

/* Ensure the operation kind is normalized and not no-op */
static void
flatpak_transaction_normalize_ops (FlatpakTransaction *self)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  GList *l, *next;

  for (l = priv->ops; l != NULL; l = next)
    {
      FlatpakTransactionOperation *op = l->data;
      next = l->next;

      if (op->kind == FLATPAK_TRANSACTION_OPERATION_INSTALL_OR_UPDATE)
        {
          g_autoptr(GBytes) deploy_data = NULL;

          if (dir_ref_is_installed (priv->dir, op->ref, NULL, &deploy_data))
            {
              /* The remote should have already been set to the installed ref
               * origin so that the resolved commit definitely exists there */
              g_assert (g_strcmp0 (op->remote, flatpak_deploy_data_get_origin (deploy_data)) == 0);

              op->kind = FLATPAK_TRANSACTION_OPERATION_UPDATE;
            }
          else
            op->kind = FLATPAK_TRANSACTION_OPERATION_INSTALL;
        }

      if (op->kind == FLATPAK_TRANSACTION_OPERATION_UPDATE &&
          !flatpak_dir_needs_update_for_commit_and_subpaths (priv->dir, op->remote, op->ref,
                                                             op->resolved_commit, (const char **) op->subpaths))
        {
          /* If this is a rebase, then at minimum a redeploy needs to happen */
          if (op->previous_ids)
            op->update_only_deploy = TRUE;
          else
            op->skip = TRUE;
        }
    }
}

static gboolean
add_uninstall_unused_ops (FlatpakTransaction  *self,
                          GCancellable        *cancellable,
                          GError             **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  g_autoptr(GHashTable) metadata_injection = NULL;
  g_autoptr(GHashTable) eol_injection = NULL;
  g_autoptr(GPtrArray) to_be_excluded = NULL;
  g_auto(GStrv) old_unused_refs = NULL;
  g_auto(GStrv) unused_refs = NULL;
  const char * const *to_be_excluded_strv = NULL;
  GList *l, *next;
  int i;

  if (priv->disable_deps)
    return TRUE;

  if (!priv->include_unused_uninstall_ops)
    {
      old_unused_refs = flatpak_dir_list_unused_refs (priv->dir,
                                                      NULL, /* arch */
                                                      NULL, /* metadata_injection */
                                                      NULL, /* eol_injection */
                                                      NULL, /* exclude_refs */
                                                      TRUE, /* filter_by_eol */
                                                      cancellable, error);
      if (old_unused_refs == NULL)
        return FALSE;
    }

  /* This is a mapping from refs to #GKeyFile metadata objects, for each ref
   * being installed or updated by the transaction. This will allows us to
   * calculate what dependencies will be used after those operations are
   * executed. For example an app update may drop an extension point and
   * thereby make an installed extension become unused. */
  metadata_injection = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL);

  eol_injection = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL);

  /* This is the set of runtimes and apps scheduled for uninstallation and
   * which are therefore excluded when calculating used refs. */
  to_be_excluded = g_ptr_array_new ();

  for (l = priv->ops; l != NULL; l = next)
    {
      FlatpakTransactionOperation *op = l->data;
      FlatpakTransactionOperationType op_type = flatpak_transaction_operation_get_operation_type (op);

      next = l->next;

      if (op->skip)
        continue;

      g_assert (op_type == FLATPAK_TRANSACTION_OPERATION_UNINSTALL ||
                op_type == FLATPAK_TRANSACTION_OPERATION_INSTALL ||
                op_type == FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE ||
                op_type == FLATPAK_TRANSACTION_OPERATION_UPDATE);

      if (op_type == FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
        g_ptr_array_add (to_be_excluded, (char *)flatpak_decomposed_get_ref (op->ref));
      else
        {
          if (op->resolved_metakey)
            g_hash_table_insert (metadata_injection, (char *)flatpak_decomposed_get_ref (op->ref), op->resolved_metakey);
          g_hash_table_insert (eol_injection, (char *)flatpak_decomposed_get_ref (op->ref),
                               GINT_TO_POINTER (op->eol != NULL || op->eol_rebase != NULL));
        }
    }

  if (to_be_excluded->len > 0)
    {
      g_ptr_array_add (to_be_excluded, NULL);
      to_be_excluded_strv = (const char * const *) to_be_excluded->pdata;
    }

  /* These are the refs that will be unused & eol after the transaction */
  unused_refs = flatpak_dir_list_unused_refs (priv->dir,
                                              NULL, /* arch */
                                              metadata_injection,
                                              eol_injection,
                                              to_be_excluded_strv,
                                              TRUE, /* filter_by_eol */
                                              cancellable, error);
  if (unused_refs == NULL)
    return FALSE;

  /* Schedule each unused runtime to be uninstalled */
  for (i = 0; unused_refs[i] != NULL; i++)
    {
      FlatpakTransactionOperation *uninstall_op;
      const char *unused_ref_str = unused_refs[i];
      g_autoptr(FlatpakDecomposed) unused_ref = flatpak_decomposed_new_from_ref (unused_ref_str, NULL);
      g_autofree char *origin = NULL;

      if (unused_ref == NULL)
        continue;

      /* Don't uninstall refs that were already unused before the transaction (unless include_unused_uninstall_ops is set) */
      if (old_unused_refs &&
          g_strv_contains ((const char * const*)old_unused_refs, flatpak_decomposed_get_ref (unused_ref)))
        continue;

      origin = flatpak_dir_get_origin (priv->dir, unused_ref, NULL, NULL);
      if (origin)
        {
          if (priv->no_deploy)
            {
              g_info ("Skipping uninstallation of %s for no deploy transaction",
                      unused_ref_str);
              continue;
            }

          /* These get added last and have no dependencies, so will run last */
          uninstall_op = flatpak_transaction_add_op (self, origin, unused_ref,
                                                     NULL, NULL, NULL, NULL,
                                                     FLATPAK_TRANSACTION_OPERATION_UNINSTALL,
                                                     FALSE);
          run_operation_last (uninstall_op);
        }
    }

  return TRUE;
}

static gboolean
flatpak_transaction_real_run (FlatpakTransaction *self,
                              GCancellable       *cancellable,
                              GError            **error)
{
  FlatpakTransactionPrivate *priv = flatpak_transaction_get_instance_private (self);
  GList *l;
  gboolean succeeded = TRUE;
  gboolean needs_prune = FALSE;
  gboolean needs_triggers = FALSE;
  gboolean needs_cache_drop = FALSE;
  gboolean ready_res = FALSE;
  int i;

  if (!priv->can_run)
    return flatpak_fail (error, _("Transaction already executed"));

  priv->can_run = FALSE;

  priv->current_op = NULL;

  if (flatpak_dir_is_user (priv->dir) && getuid () == 0)
    {
      struct stat st_buf;
      g_autofree char *dir_path = NULL;

      /* Check that it's not root's own user installation */
      dir_path = g_file_get_path (flatpak_dir_get_path (priv->dir));
      if (stat (dir_path, &st_buf) == 0 && st_buf.st_uid != 0)
        return flatpak_fail_error (error, FLATPAK_ERROR_WRONG_USER,
                                   _("Refusing to operate on a user installation as root! "
                                     "This can lead to incorrect file ownership and permission errors."));
    }

  if (!priv->no_pull &&
      !flatpak_transaction_update_metadata (self, cancellable, error))
    {
      g_assert (error == NULL || *error != NULL);
      return FALSE;
    }

  if (!flatpak_transaction_add_auto_install (self, cancellable, error))
    {
      g_assert (error == NULL || *error != NULL);
      return FALSE;
    }

  if (!flatpak_transaction_resolve_flatpakrefs (self, cancellable, error))
    {
      g_assert (error == NULL || *error != NULL);
      return FALSE;
    }

  if (!flatpak_transaction_resolve_bundles (self, cancellable, error))
    {
      g_assert (error == NULL || *error != NULL);
      return FALSE;
    }

  /* Resolve initial ops */
  if (!resolve_all_ops (self, cancellable, error))
    {
      g_assert (error == NULL || *error != NULL);
      return FALSE;
    }

  /* Add all app -> runtime dependencies */
  for (l = priv->ops; l != NULL; l = l->next)
    {
      FlatpakTransactionOperation *op = l->data;

      if (!op->skip && !add_deps (self, op, error))
        {
          g_assert (error == NULL || *error != NULL);
          return FALSE;
        }
    }

  /* Resolve new ops */
  if (!resolve_all_ops (self, cancellable, error))
    {
      g_assert (error == NULL || *error != NULL);
      return FALSE;
    }

  /* Add all related extensions */
  for (l = priv->ops; l != NULL; l = l->next)
    {
      FlatpakTransactionOperation *op = l->data;

      if (!op->skip && !add_related (self, op, error))
        {
          g_assert (error == NULL || *error != NULL);
          return FALSE;
        }
    }

  /* Resolve new ops */
  if (!resolve_all_ops (self, cancellable, error))
    {
      g_assert (error == NULL || *error != NULL);
      return FALSE;
    }

  /* Ensure the operation kind is normalized and not no-op */
  flatpak_transaction_normalize_ops (self);

  /* Add uninstall ops for things that are made unused by this transaction (and
   * which match a heuristic). We don't need to do another round of
   * resolve_all_ops() since uninstalls don't require that.
   */
  if (!add_uninstall_unused_ops (self, cancellable, error))
    {
      g_assert (error == NULL || *error != NULL);
      return FALSE;
    }

  sort_ops (self);

  ready_res = FALSE;
  g_signal_emit (self, signals[READY_PRE_AUTH], 0, &ready_res);
  if (!ready_res)
    return flatpak_fail_error (error, FLATPAK_ERROR_ABORTED, _("Aborted by user"));

  /* Ensure we have all required tokens; we do this after all resolves if
   * possible to bunch requests. */
  if (!request_required_tokens (self, NULL, cancellable, error))
    {
      g_assert (error == NULL || *error != NULL);
      return FALSE;
    }

  ready_res = FALSE;
  g_signal_emit (self, signals[READY], 0, &ready_res);
  if (!ready_res)
    return flatpak_fail_error (error, FLATPAK_ERROR_ABORTED, _("Aborted by user"));

  for (l = priv->ops; l != NULL; l = l->next)
    {
      FlatpakTransactionOperation *op = l->data;
      g_autoptr(GError) local_error = NULL;
      gboolean res = TRUE;
      const char *pref;
      g_autoptr(FlatpakRemoteState) state = NULL;

      if (op->skip)
        continue;

      priv->current_op = op;

      pref = flatpak_decomposed_get_pref (op->ref);

      if (op->fail_if_op_fails && (op->fail_if_op_fails->failed) &&
          /* Allow installing an app if the runtime failed to update (i.e. is installed) because
           * the app should still run, and otherwise you could never install the app until the runtime
           * remote is fixed. */
          !(op->fail_if_op_fails->kind == FLATPAK_TRANSACTION_OPERATION_UPDATE &&
            flatpak_decomposed_is_app (op->ref)))
        {
          flatpak_fail_error (&local_error, FLATPAK_ERROR_SKIPPED,
                              _("Skipping %s due to previous error"), pref);
          res = FALSE;
        }
      else if (op->kind != FLATPAK_TRANSACTION_OPERATION_UNINSTALL &&
               (state = flatpak_transaction_ensure_remote_state (self, op->kind, op->remote, NULL, &local_error)) == NULL)
        {
          res = FALSE;
        }

      /* Here we execute the operation in a helper function */
      if (res && !_run_op_kind (self, op, state,
                                &needs_prune, &needs_triggers, &needs_cache_drop,
                                cancellable, &local_error))
        res = FALSE;

      if (res)
        {
          g_autoptr(GBytes) deploy_data = NULL;
          /* deploy v4 guarantees eol/eolr info */
          deploy_data = flatpak_dir_get_deploy_data (priv->dir, op->ref, 4, NULL, NULL);

          if (deploy_data)
            {
              const char *eol =  flatpak_deploy_data_get_eol (deploy_data);
              const char *eol_rebase = flatpak_deploy_data_get_eol_rebase (deploy_data);

              if (eol || eol_rebase)
                g_signal_emit (self, signals[END_OF_LIFED], 0,
                               flatpak_decomposed_get_ref (op->ref), eol, eol_rebase);
            }
        }

      if (!res)
        {
          gboolean do_cont = FALSE;
          FlatpakTransactionErrorDetails error_details = 0;

          op->failed = TRUE;

          if (op->non_fatal)
            error_details |= FLATPAK_TRANSACTION_ERROR_DETAILS_NON_FATAL;

          g_signal_emit (self, signals[OPERATION_ERROR], 0, op,
                         local_error, error_details,
                         &do_cont);

          if (!do_cont)
            {
              if (g_cancellable_set_error_if_cancelled (cancellable, error))
                {
                  succeeded = FALSE;
                  break;
                }

              flatpak_fail_error (error, FLATPAK_ERROR_ABORTED, _("Aborted due to failure (%s)"), local_error->message);
              succeeded = FALSE;
              break;
            }
        }
    }
  priv->current_op = NULL;

  if (needs_triggers)
    flatpak_dir_run_triggers (priv->dir, cancellable, NULL);

  if (needs_prune && !priv->disable_prune)
    flatpak_dir_prune (priv->dir, cancellable, NULL);

  for (i = 0; i < priv->added_origin_remotes->len; i++)
    flatpak_dir_prune_origin_remote (priv->dir, g_ptr_array_index (priv->added_origin_remotes, i));

  /* Reload config in case it changed via system helper */
  if (needs_cache_drop || priv->added_origin_remotes->len > 0)
    flatpak_installation_drop_caches (priv->installation, NULL, NULL);

  return succeeded;
}
