[evolution-data-server/wip/offline-cache] [ECalMetaBackend] Implement refresh with offline changes storing



commit c2e29a6ddbe1ebc11b4608305348f08b6362b451
Author: Milan Crha <mcrha redhat com>
Date:   Fri Mar 3 14:01:33 2017 +0100

    [ECalMetaBackend] Implement refresh with offline changes storing

 src/calendar/libedata-cal/e-cal-cache.c        |  131 ++++++++++++
 src/calendar/libedata-cal/e-cal-cache.h        |   37 ++++
 src/calendar/libedata-cal/e-cal-meta-backend.c |  270 +++++++++++++++++++-----
 src/libebackend/e-cache.h                      |   11 +
 4 files changed, 396 insertions(+), 53 deletions(-)
---
diff --git a/src/calendar/libedata-cal/e-cal-cache.c b/src/calendar/libedata-cal/e-cal-cache.c
index b51f848..1c7e45f 100644
--- a/src/calendar/libedata-cal/e-cal-cache.c
+++ b/src/calendar/libedata-cal/e-cal-cache.c
@@ -88,9 +88,89 @@ G_DEFINE_TYPE_WITH_CODE (ECalCache, e_cal_cache, E_TYPE_CACHE,
                         G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE, NULL)
                         G_IMPLEMENT_INTERFACE (E_TYPE_TIMEZONE_CACHE, ecc_timezone_cache_init))
 
+G_DEFINE_BOXED_TYPE (ECalCacheOfflineChange, e_cal_cache_offline_change, e_cal_cache_offline_change_copy, 
e_cal_cache_offline_change_free)
 G_DEFINE_BOXED_TYPE (ECalCacheSearchData, e_cal_cache_search_data, e_cal_cache_search_data_copy, 
e_cal_cache_search_data_free)
 
 /**
+ * e_cal_cache_offline_change_new:
+ * @uid: a unique component identifier
+ * @rid: (nullable):  a Recurrence-ID of the component
+ * @revision: (nullable): a revision of the component
+ * @object: (nullable): component itself
+ * @state: an #EOfflineState
+ *
+ * Creates a new #ECalCacheOfflineChange with the offline @state
+ * information for the given @uid.
+ *
+ * Returns: (transfer full): A new #ECalCacheOfflineChange. Free it with
+ *    e_cal_cache_offline_change_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECalCacheOfflineChange *
+e_cal_cache_offline_change_new (const gchar *uid,
+                               const gchar *rid,
+                               const gchar *revision,
+                               const gchar *object,
+                               EOfflineState state)
+{
+       ECalCacheOfflineChange *change;
+
+       g_return_val_if_fail (uid != NULL, NULL);
+
+       change = g_new0 (ECalCacheOfflineChange, 1);
+       change->uid = g_strdup (uid);
+       change->rid = g_strdup (rid);
+       change->revision = g_strdup (revision);
+       change->object = g_strdup (object);
+       change->state = state;
+
+       return change;
+}
+
+/**
+ * e_cal_cache_offline_change_copy:
+ * @change: (nullable): a source #ECalCacheOfflineChange to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @change. Free it with
+ *    e_cal_cache_offline_change_free() when no longer needed.
+ *    If the @change is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+ECalCacheOfflineChange *
+e_cal_cache_offline_change_copy (const ECalCacheOfflineChange *change)
+{
+       if (!change)
+               return NULL;
+
+       return e_cal_cache_offline_change_new (change->uid, change->rid, change->revision, change->object, 
change->state);
+}
+
+/**
+ * e_cal_cache_offline_change_free:
+ * @change: (nullable): an #ECalCacheOfflineChange
+ *
+ * Frees the @change structure, previously allocated with e_cal_cache_offline_change_new()
+ * or e_cal_cache_offline_change_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_cache_offline_change_free (gpointer change)
+{
+       ECalCacheOfflineChange *chng = change;
+
+       if (chng) {
+               g_free (chng->uid);
+               g_free (chng->rid);
+               g_free (chng->revision);
+               g_free (chng->object);
+               g_free (chng);
+       }
+}
+
+/**
  * e_cal_cache_search_data_new:
  * @uid: a component UID; cannot be %NULL
  * @rid: (nullable): a component Recurrence-ID; can be %NULL
@@ -2672,6 +2752,57 @@ e_cal_cache_search_with_callback (ECalCache *cal_cache,
 }
 
 /**
+ * e_cal_cache_get_offline_changes:
+ * @cal_cache: an #ECalCache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * The same as e_cache_get_offline_changes(), only splits the saved UID
+ * into UID and RID and saved the data into #ECalCacheOfflineChange structure.
+ *
+ * Returns: (transfer full) (element-type ECalCacheOfflineChange): A newly allocated list of all
+ *    offline changes. Free it with g_slist_free_full (slist, e_cal_cache_offline_change_free);
+ *    when no longer needed.
+ *
+ * Since: 3.26
+ **/
+GSList *
+e_cal_cache_get_offline_changes        (ECalCache *cal_cache,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       GSList *changes, *link;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), NULL);
+
+       changes = e_cache_get_offline_changes (E_CACHE (cal_cache), cancellable, error);
+
+       for (link = changes; link; link = g_slist_next (link)) {
+               ECacheOfflineChange *cache_change = link->data;
+               ECalCacheOfflineChange *cal_change;
+               gchar *uid = NULL, *rid = NULL;
+
+               if (!cache_change || !ecc_decode_id_sql (cache_change->uid, &uid, &rid)) {
+                       g_warn_if_reached ();
+
+                       e_cache_offline_change_free (cache_change);
+                       link->data = NULL;
+
+                       continue;
+               }
+
+               cal_change = e_cal_cache_offline_change_new (uid, rid, cache_change->revision, 
cache_change->object, cache_change->state);
+               link->data = cal_change;
+
+               e_cache_offline_change_free (cache_change);
+               g_free (uid);
+               g_free (rid);
+       }
+
+       return changes;
+}
+
+/**
  * e_cal_cache_put_timezone:
  * @cal_cache: an #ECalCache
  * @zone: an icaltimezone to put
diff --git a/src/calendar/libedata-cal/e-cal-cache.h b/src/calendar/libedata-cal/e-cal-cache.h
index 4f80a79..9dd1476 100644
--- a/src/calendar/libedata-cal/e-cal-cache.h
+++ b/src/calendar/libedata-cal/e-cal-cache.h
@@ -51,6 +51,40 @@ typedef struct _ECalCacheClass ECalCacheClass;
 typedef struct _ECalCachePrivate ECalCachePrivate;
 
 /**
+ * ECalCacheOfflineChange:
+ * @uid: UID of the component
+ * @rid: Recurrence-ID of the component
+ * @revision: stored revision of the component
+ * @object: the component itself, as iCalalendar string
+ * @state: an #EOfflineState of the component
+ *
+ * Holds the information about offline change for one component.
+ *
+ * Since: 3.26
+ **/
+typedef struct {
+       gchar *uid;
+       gchar *rid;
+       gchar *revision;
+       gchar *object;
+       EOfflineState state;
+} ECalCacheOfflineChange;
+
+#define E_TYPE_CAL_CACHE_OFFLINE_CHANGE (e_cal_cache_offline_change_get_type ())
+
+GType          e_cal_cache_offline_change_get_type
+                                               (void) G_GNUC_CONST;
+ECalCacheOfflineChange *
+               e_cal_cache_offline_change_new  (const gchar *uid,
+                                                const gchar *rid,
+                                                const gchar *revision,
+                                                const gchar *object,
+                                                EOfflineState state);
+ECalCacheOfflineChange *
+               e_cal_cache_offline_change_copy (const ECalCacheOfflineChange *change);
+void           e_cal_cache_offline_change_free (/* ECalCacheOfflineChange */ gpointer change);
+
+/**
  * ECalCacheSearchData:
  * @uid: the UID of this component
  * @rid: (nullable): the Recurrence-ID of this component
@@ -258,6 +292,9 @@ gboolean    e_cal_cache_search_with_callback
                                                 gpointer user_data,
                                                 GCancellable *cancellable,
                                                 GError **error);
+GSList *       e_cal_cache_get_offline_changes (ECalCache *cal_cache,
+                                                GCancellable *cancellable,
+                                                GError **error);
 
 gboolean       e_cal_cache_put_timezone        (ECalCache *cal_cache,
                                                 const icaltimezone *zone,
diff --git a/src/calendar/libedata-cal/e-cal-meta-backend.c b/src/calendar/libedata-cal/e-cal-meta-backend.c
index 3f46426..067628e 100644
--- a/src/calendar/libedata-cal/e-cal-meta-backend.c
+++ b/src/calendar/libedata-cal/e-cal-meta-backend.c
@@ -43,6 +43,9 @@
 #include "e-cal-backend-util.h"
 #include "e-cal-meta-backend.h"
 
+#define ECMB_KEY_SYNC_TAG "sync-tag"
+#define ECMB_KEY_DID_CONNECT "did-connect"
+
 #define LOCAL_PREFIX "file://"
 
 struct _ECalMetaBackendPrivate {
@@ -71,6 +74,21 @@ G_DEFINE_BOXED_TYPE (ECalMetaBackendInfo, e_cal_meta_backend_info, e_cal_meta_ba
 static void ecmb_schedule_refresh (ECalMetaBackend *meta_backend);
 static void ecmb_schedule_source_changed (ECalMetaBackend *meta_backend);
 static void ecmb_schedule_go_offline (ECalMetaBackend *meta_backend);
+static gboolean ecmb_load_component_wrapper_sync (ECalMetaBackend *meta_backend,
+                                                 ECalCache *cal_cache,
+                                                 const gchar *uid,
+                                                 GCancellable *cancellable,
+                                                 GError **error);
+static gboolean ecmb_save_component_wrapper_sync (ECalMetaBackend *meta_backend,
+                                                 ECalCache *cal_cache,
+                                                 gboolean overwrite_existing,
+                                                 EConflictResolution conflict_resolution,
+                                                 const GSList *in_instances,
+                                                 const gchar *extra,
+                                                 const gchar *orig_uid,
+                                                 gboolean *requires_put,
+                                                 GCancellable *cancellable,
+                                                 GError **error);
 
 /**
  * e_cal_cache_search_data_new:
@@ -350,6 +368,83 @@ ecmb_start_view_thread_func (ECalBackend *cal_backend,
        }
 }
 
+static gboolean
+ecmb_upload_local_changes_sync (ECalMetaBackend *meta_backend,
+                               ECalCache *cal_cache,
+                               EConflictResolution conflict_resolution,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       GSList *offline_changes, *link;
+       GHashTable *covered_uids;
+       ECache *cache;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+
+       cache = E_CACHE (cal_cache);
+       covered_uids = g_hash_table_new (g_str_hash, g_str_equal);
+
+       offline_changes = e_cal_cache_get_offline_changes (cal_cache, cancellable, error);
+       for (link = offline_changes; link && success; link = g_slist_next (link)) {
+               ECalCacheOfflineChange *change = link->data;
+               gchar *extra = NULL;
+
+               success = !g_cancellable_set_error_if_cancelled (cancellable, error);
+               if (!success)
+                       break;
+
+               if (!change || g_hash_table_contains (covered_uids, change->uid))
+                       continue;
+
+               g_hash_table_insert (covered_uids, change->uid, NULL);
+
+               if (!e_cal_cache_get_component_extra (cal_cache, change->uid, NULL, &extra, cancellable, 
NULL))
+                       extra = NULL;
+
+               if (change->state == E_OFFLINE_STATE_LOCALLY_CREATED ||
+                   change->state == E_OFFLINE_STATE_LOCALLY_MODIFIED) {
+                       GSList *instances = NULL;
+
+                       success = e_cal_cache_get_components_by_uid (cal_cache, change->uid, &instances, 
cancellable, error);
+                       if (success) {
+                               success = ecmb_save_component_wrapper_sync (meta_backend, cal_cache,
+                                       change->state == E_OFFLINE_STATE_LOCALLY_MODIFIED,
+                                       conflict_resolution, instances, extra, change->uid, NULL, 
cancellable, error);
+                       }
+
+                       g_slist_free_full (instances, g_object_unref);
+               } else if (change->state == E_OFFLINE_STATE_LOCALLY_DELETED) {
+                       GError *local_error = NULL;
+
+                       success = e_cal_meta_backend_remove_component_sync (meta_backend, conflict_resolution,
+                               change->uid, extra, cancellable, &local_error);
+
+                       if (!success) {
+                               if (g_error_matches (local_error, E_DATA_CAL_ERROR, ObjectNotFound)) {
+                                       g_clear_error (&local_error);
+                                       success = TRUE;
+                               } else if (local_error) {
+                                       g_propagate_error (error, local_error);
+                               }
+                       }
+               } else {
+                       g_warn_if_reached ();
+               }
+
+               g_free (extra);
+       }
+
+       g_slist_free_full (offline_changes, e_cal_cache_offline_change_free);
+       g_hash_table_destroy (covered_uids);
+
+       if (success)
+               success = e_cache_clear_offline_changes (cache, cancellable, error);
+
+       return success;
+}
+
 static void
 ecmb_refresh_thread_func (ECalBackend *cal_backend,
                          gpointer user_data,
@@ -357,6 +452,9 @@ ecmb_refresh_thread_func (ECalBackend *cal_backend,
                          GError **error)
 {
        ECalMetaBackend *meta_backend;
+       ECalCache *cal_cache;
+       ECacheOfflineFlag offline_flag = E_CACHE_IS_ONLINE;
+       gboolean success, repeat = TRUE;
 
        g_return_if_fail (E_IS_CAL_META_BACKEND (cal_backend));
 
@@ -365,8 +463,104 @@ ecmb_refresh_thread_func (ECalBackend *cal_backend,
 
        meta_backend = E_CAL_META_BACKEND (cal_backend);
 
-       if (!e_backend_get_online (E_BACKEND (meta_backend)))
+       if (!e_backend_get_online (E_BACKEND (meta_backend)) ||
+           !e_cal_meta_backend_connect_sync (meta_backend, cancellable, NULL)) {
+               /* Ignore connection errors here */
                return;
+       }
+
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       if (!cal_cache) {
+               return;
+       }
+
+       success = ecmb_upload_local_changes_sync (meta_backend, cal_cache, E_CONFLICT_RESOLUTION_KEEP_LOCAL, 
cancellable, error);
+
+       while (repeat && success &&
+              !g_cancellable_set_error_if_cancelled (cancellable, error)) {
+               GSList *created_objects = NULL, *modified_objects = NULL, *removed_objects = NULL, *link;
+               gchar *last_sync_tag, *new_sync_tag = NULL;
+
+               repeat = FALSE;
+
+               last_sync_tag = e_cache_dup_key (E_CACHE (cal_cache), ECMB_KEY_SYNC_TAG, NULL);
+               if (last_sync_tag && !*last_sync_tag) {
+                       g_free (last_sync_tag);
+                       last_sync_tag = NULL;
+               }
+
+               success = e_cal_meta_backend_get_changes_sync (meta_backend, last_sync_tag, &new_sync_tag, 
&repeat,
+                       &created_objects, &modified_objects, &removed_objects, cancellable, error);
+
+               if (success) {
+                       GHashTable *covered_uids;
+
+                       covered_uids = g_hash_table_new (g_str_hash, g_str_equal);
+
+                       /* Removed objects first */
+                       for (link = removed_objects; link && success; link = g_slist_next (link)) {
+                               ECalMetaBackendInfo *nfo = link->data;
+                               ECalComponentId id;
+
+                               if (!nfo) {
+                                       g_warn_if_reached ();
+                                       continue;
+                               }
+
+                               id.uid = nfo->uid;
+                               id.rid = nfo->rid;
+
+                               success = e_cal_cache_remove_component (cal_cache, id.uid, id.rid, 
offline_flag, cancellable, error);
+                               if (success)
+                                       e_cal_backend_notify_component_removed (cal_backend, &id, NULL, NULL);
+                       }
+
+                       /* Then modified objects */
+                       for (link = modified_objects; link && success; link = g_slist_next (link)) {
+                               ECalMetaBackendInfo *nfo = link->data;
+
+                               if (!nfo || !nfo->uid) {
+                                       g_warn_if_reached ();
+                                       continue;
+                               }
+
+                               if (g_hash_table_contains (covered_uids, nfo->uid))
+                                       continue;
+
+                               g_hash_table_insert (covered_uids, nfo->uid, NULL);
+
+                               success = ecmb_load_component_wrapper_sync (meta_backend, cal_cache, 
nfo->uid, cancellable, error);
+                       }
+
+                       g_hash_table_remove_all (covered_uids);
+
+                       /* Finally created objects */
+                       for (link = created_objects; link && success; link = g_slist_next (link)) {
+                               ECalMetaBackendInfo *nfo = link->data;
+
+                               if (!nfo || !nfo->uid) {
+                                       g_warn_if_reached ();
+                                       continue;
+                               }
+
+                               success = ecmb_load_component_wrapper_sync (meta_backend, cal_cache, 
nfo->uid, cancellable, error);
+                       }
+
+                       g_hash_table_destroy (covered_uids);
+               }
+
+               if (success) {
+                       e_cache_set_key (E_CACHE (cal_cache), ECMB_KEY_SYNC_TAG, new_sync_tag ? new_sync_tag 
: "", NULL);
+               }
+
+               g_slist_free_full (created_objects, e_cal_meta_backend_info_free);
+               g_slist_free_full (modified_objects, e_cal_meta_backend_info_free);
+               g_slist_free_full (removed_objects, e_cal_meta_backend_info_free);
+               g_free (last_sync_tag);
+               g_free (new_sync_tag);
+       }
+
+       g_object_unref (cal_cache);
 }
 
 static void
@@ -713,15 +907,18 @@ ecmb_load_component_wrapper_sync (ECalMetaBackend *meta_backend,
 
 static gboolean
 ecmb_save_component_wrapper_sync (ECalMetaBackend *meta_backend,
+                                 ECalCache *cal_cache,
                                  gboolean overwrite_existing,
                                  EConflictResolution conflict_resolution,
                                  const GSList *in_instances,
                                  const gchar *extra,
-                                 gchar **out_new_uid,
+                                 const gchar *orig_uid,
+                                 gboolean *out_requires_put,
                                  GCancellable *cancellable,
                                  GError **error)
 {
        GSList *link, *instances = NULL;
+       gchar *new_uid = NULL;
        gboolean has_attachments = FALSE, success = TRUE;
 
        for (link = (GSList *) in_instances; link && !has_attachments; link = g_slist_next (link)) {
@@ -748,7 +945,19 @@ ecmb_save_component_wrapper_sync (ECalMetaBackend *meta_backend,
        }
 
        success = success && e_cal_meta_backend_save_component_sync (meta_backend, overwrite_existing, 
conflict_resolution,
-               instances ? instances : in_instances, extra, out_new_uid, cancellable, error);
+               instances ? instances : in_instances, extra, &new_uid, cancellable, error);
+
+       if (success && new_uid) {
+               success = ecmb_load_component_wrapper_sync (meta_backend, cal_cache, new_uid, cancellable, 
error);
+
+               if (success && g_strcmp0 (new_uid, orig_uid) != 0)
+                   success = ecmb_maybe_remove_from_cache (meta_backend, cal_cache, E_CACHE_IS_ONLINE, 
orig_uid, cancellable, error);
+
+               g_free (new_uid);
+
+               if (out_requires_put)
+                       *out_requires_put = FALSE;
+       }
 
        g_slist_free_full (instances, g_object_unref);
 
@@ -1084,33 +1293,15 @@ ecmb_create_object_sync (ECalMetaBackend *meta_backend,
 
        if (*offline_flag == E_CACHE_IS_ONLINE) {
                GSList *instances;
-               gchar *new_uid = NULL;
 
                instances = g_slist_prepend (NULL, comp);
 
-               if (!ecmb_save_component_wrapper_sync (meta_backend, FALSE, conflict_resolution, instances, 
NULL, &new_uid, cancellable, error)) {
+               if (!ecmb_save_component_wrapper_sync (meta_backend, cal_cache, FALSE, conflict_resolution, 
instances, NULL, uid, &requires_put, cancellable, error)) {
                        g_slist_free (instances);
                        return FALSE;
                }
 
                g_slist_free (instances);
-
-               if (new_uid) {
-                       if (!ecmb_load_component_wrapper_sync (meta_backend, cal_cache, new_uid, cancellable, 
error)) {
-                               g_free (new_uid);
-                               return FALSE;
-                       }
-
-                       if (g_strcmp0 (new_uid, uid) != 0 &&
-                           !ecmb_maybe_remove_from_cache (meta_backend, cal_cache, *offline_flag, uid, 
cancellable, error)) {
-                               g_free (new_uid);
-                               return FALSE;
-                       }
-
-                       g_free (new_uid);
-
-                       requires_put = FALSE;
-               }
        }
 
        if (requires_put) {
@@ -1384,21 +1575,8 @@ ecmb_modify_object_sync (ECalMetaBackend *meta_backend,
        }
 
        if (success && *offline_flag == E_CACHE_IS_ONLINE) {
-               gchar *new_uid = NULL;
-
-               success = ecmb_save_component_wrapper_sync (meta_backend, TRUE, conflict_resolution,
-                       instances, extra, &new_uid, cancellable, error);
-
-               if (success && new_uid) {
-                       success = ecmb_load_component_wrapper_sync (meta_backend, cal_cache, new_uid, 
cancellable, error);
-
-                       if (success && g_strcmp0 (new_uid, id->uid) != 0)
-                           success = ecmb_maybe_remove_from_cache (meta_backend, cal_cache, *offline_flag, 
id->uid, cancellable, error);
-
-                       g_free (new_uid);
-
-                       requires_put = FALSE;
-               }
+               success = ecmb_save_component_wrapper_sync (meta_backend, cal_cache, TRUE, 
conflict_resolution,
+                       instances, extra, id->uid, &requires_put, cancellable, error);
        }
 
        if (success && requires_put)
@@ -1688,26 +1866,12 @@ ecmb_remove_object_sync (ECalMetaBackend *meta_backend,
                        }
 
                        if (*offline_flag == E_CACHE_IS_ONLINE) {
-                               gchar *new_uid = NULL;
-
-                               success = ecmb_save_component_wrapper_sync (meta_backend, TRUE, 
conflict_resolution,
-                                       instances, extra, &new_uid, cancellable, error);
-
-                               if (success && new_uid) {
-                                       success = ecmb_load_component_wrapper_sync (meta_backend, cal_cache, 
new_uid, cancellable, error);
-
-                                       if (success && g_strcmp0 (new_uid, uid) != 0)
-                                           success = ecmb_maybe_remove_from_cache (meta_backend, cal_cache, 
*offline_flag, uid, cancellable, error);
-
-                                       g_free (new_uid);
-
-                                       requires_put = FALSE;
-                               }
+                               success = ecmb_save_component_wrapper_sync (meta_backend, cal_cache, TRUE, 
conflict_resolution,
+                                       instances, extra, uid, &requires_put, cancellable, error);
                        }
 
                        if (success && requires_put)
                                success = ecmb_put_instances (meta_backend, cal_cache, uid, *offline_flag, 
instances, extra, cancellable, error);
-
                }
 
                g_free (extra);
diff --git a/src/libebackend/e-cache.h b/src/libebackend/e-cache.h
index 90ce333..ca10faf 100644
--- a/src/libebackend/e-cache.h
+++ b/src/libebackend/e-cache.h
@@ -123,6 +123,17 @@ guint              e_cache_column_values_get_size  (ECacheColumnValues *other_columns);
 void           e_cache_column_values_init_iter (ECacheColumnValues *other_columns,
                                                 GHashTableIter *iter);
 
+/**
+ * ECacheOfflineChange:
+ * @uid: UID of the object
+ * @revision: stored revision of the object
+ * @object: the object itself
+ * @state: an #EOfflineState of the object
+ *
+ * Holds the information about offline change for one object.
+ *
+ * Since: 3.26
+ **/
 typedef struct {
        gchar *uid;
        gchar *revision;


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]