lib/repo: Add API to create and list ref aliases
authorColin Walters <walters@verbum.org>
Fri, 28 Jul 2017 01:22:48 +0000 (21:22 -0400)
committerAtomic Bot <atomic-devel@projectatomic.io>
Wed, 2 Aug 2017 17:33:10 +0000 (17:33 +0000)
There are multiple use cases where we'd like to alias refs.

First, having a "stable" alias which gets swapped across major
versions: https://pagure.io/atomic-wg/issue/228

Another case is when a ref is obsoleted;
<https://pagure.io/atomic-wg/issue/303>
This second one could be done with endoflife rebase, but I think
this case is better on the server side, as we might later change
our minds and do actual releases there.

I initially just added some test cases for symlinks in the `refs/heads` dir to
ensure this actually works (and it did), but I think it's worth having APIs.

Closes: #1033
Approved by: jlebon

apidoc/ostree-sections.txt
src/libostree/libostree-devel.sym
src/libostree/ostree-repo-commit.c
src/libostree/ostree-repo-private.h
src/libostree/ostree-repo-refs.c
src/libostree/ostree-repo.h
src/ostree/ot-builtin-refs.c
tests/test-refs.sh

index 47f351d226b27733f533314abc9e2f0d8c113507..43e267f6002ca3db3e1f88cc5fa4458053cd6250 100644 (file)
@@ -307,6 +307,7 @@ ostree_repo_abort_transaction
 ostree_repo_transaction_set_refspec
 ostree_repo_transaction_set_ref
 ostree_repo_set_ref_immediate
+ostree_repo_set_alias_ref_immediate
 ostree_repo_set_cache_dir
 ostree_repo_sign_delta
 ostree_repo_has_object
index c93e388ae80017b250ffab2562469b328c2d20ec..d4ee86bf2e69410788747db214197b9aebe91ae3 100644 (file)
@@ -19,6 +19,7 @@
 
 /* Add new symbols here.  Release commits should copy this section into -released.sym. */
 LIBOSTREE_2017.10 {
+  ostree_repo_set_alias_ref_immediate;
 };
 
 /* Stub section for the stable release *after* this development one; don't
index ab348311e7450a01de39c6bae93ce216f5e68aa1..37b14f26d7461ac19d28abef95ac22d2caec0379 100644 (file)
@@ -1451,7 +1451,31 @@ ostree_repo_set_ref_immediate (OstreeRepo *self,
                                GError       **error)
 {
   const OstreeCollectionRef _ref = { NULL, (gchar *) ref };
-  return _ostree_repo_write_ref (self, remote, &_ref, checksum,
+  return _ostree_repo_write_ref (self, remote, &_ref, checksum, NULL,
+                                 cancellable, error);
+}
+
+/**
+ * ostree_repo_set_alias_ref_immediate:
+ * @self: An #OstreeRepo
+ * @remote: (allow-none): A remote for the ref
+ * @ref: The ref to write
+ * @target: (allow-none): The ref target to point it to, or %NULL to unset
+ * @cancellable: GCancellable
+ * @error: GError
+ *
+ * Like ostree_repo_set_ref_immediate(), but creates an alias.
+ */
+gboolean
+ostree_repo_set_alias_ref_immediate (OstreeRepo *self,
+                                     const char *remote,
+                                     const char *ref,
+                                     const char *target,
+                                     GCancellable  *cancellable,
+                                     GError       **error)
+{
+  const OstreeCollectionRef _ref = { NULL, (gchar *) ref };
+  return _ostree_repo_write_ref (self, remote, &_ref, NULL, target,
                                  cancellable, error);
 }
 
@@ -1483,7 +1507,7 @@ ostree_repo_set_collection_ref_immediate (OstreeRepo                 *self,
   g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE);
   g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
 
-  return _ostree_repo_write_ref (self, NULL, ref, checksum,
+  return _ostree_repo_write_ref (self, NULL, ref, checksum, NULL,
                                  cancellable, error);
 }
 
index 5dd0dea701881e44be6dc2d9b5471fd09590f209..1596c0db6e9c81fa61e32b0025689c8d808a42c2 100644 (file)
@@ -265,6 +265,7 @@ _ostree_repo_write_ref (OstreeRepo                 *self,
                         const char                 *remote,
                         const OstreeCollectionRef  *ref,
                         const char                 *rev,
+                        const char                 *alias,
                         GCancellable               *cancellable,
                         GError                    **error);
 
index 491f22bd236bb3d86bd79c3aabc88df8aaee0dff..a180e40ba0629619013e7367446da9857428b341 100644 (file)
@@ -472,6 +472,7 @@ ostree_repo_resolve_rev_ext (OstreeRepo                    *self,
 static gboolean
 enumerate_refs_recurse (OstreeRepo    *repo,
                         const char    *remote,
+                        OstreeRepoListRefsExtFlags flags,
                         const char    *collection_id,
                         int            base_dfd,
                         GString       *base_path,
@@ -482,6 +483,7 @@ enumerate_refs_recurse (OstreeRepo    *repo,
                         GError       **error)
 {
   g_auto(GLnxDirFdIterator) dfd_iter = { 0, };
+  const gboolean aliases_only = (flags & OSTREE_REPO_LIST_REFS_EXT_ALIASES) > 0;
 
   if (!glnx_dirfd_iterator_init_at (child_dfd, path, FALSE, &dfd_iter, error))
     return FALSE;
@@ -502,16 +504,30 @@ enumerate_refs_recurse (OstreeRepo    *repo,
         {
           g_string_append_c (base_path, '/');
 
-          if (!enumerate_refs_recurse (repo, remote, collection_id, base_dfd, base_path,
+          if (!enumerate_refs_recurse (repo, remote, flags, collection_id, base_dfd, base_path,
                                        dfd_iter.fd, dent->d_name,
                                        refs, cancellable, error))
             return FALSE;
         }
-      else if (dent->d_type == DT_REG)
+      else
         {
-          if (!add_ref_to_set (remote, collection_id, base_dfd, base_path->str, refs,
-                               cancellable, error))
-            return FALSE;
+          if (aliases_only && dent->d_type == DT_LNK)
+            {
+              g_autofree char *target = glnx_readlinkat_malloc (base_dfd, base_path->str,
+                                                                cancellable, error);
+              const char *resolved_target = target;
+              if (!target)
+                return FALSE;
+              while (g_str_has_prefix (resolved_target, "../"))
+                resolved_target += 3;
+              g_hash_table_insert (refs, g_strdup (base_path->str), g_strdup (resolved_target));
+            }
+          else if ((!aliases_only && dent->d_type == DT_REG) || dent->d_type == DT_LNK)
+            {
+              if (!add_ref_to_set (remote, collection_id, base_dfd, base_path->str, refs,
+                                   cancellable, error))
+                return FALSE;
+            }
         }
 
       g_string_truncate (base_path, len);
@@ -523,6 +539,7 @@ enumerate_refs_recurse (OstreeRepo    *repo,
 static gboolean
 _ostree_repo_list_refs_internal (OstreeRepo       *self,
                                  gboolean         cut_prefix,
+                                 OstreeRepoListRefsExtFlags flags,
                                  const char       *refspec_prefix,
                                  GHashTable      **out_all_refs,
                                  GCancellable     *cancellable,
@@ -571,7 +588,7 @@ _ostree_repo_list_refs_internal (OstreeRepo       *self,
               if (!glnx_opendirat (self->repo_dir_fd, cut_prefix ? path : prefix_path, TRUE, &base_fd, error))
                 return FALSE;
 
-              if (!enumerate_refs_recurse (self, remote, NULL, base_fd, base_path,
+              if (!enumerate_refs_recurse (self, remote, flags, NULL, base_fd, base_path,
                                            base_fd, cut_prefix ? "." : ref_prefix,
                                            ret_all_refs, cancellable, error))
                 return FALSE;
@@ -598,7 +615,7 @@ _ostree_repo_list_refs_internal (OstreeRepo       *self,
       if (!glnx_opendirat (self->repo_dir_fd, "refs/heads", TRUE, &refs_heads_dfd, error))
         return FALSE;
 
-      if (!enumerate_refs_recurse (self, NULL, NULL, refs_heads_dfd, base_path,
+      if (!enumerate_refs_recurse (self, NULL, flags, NULL, refs_heads_dfd, base_path,
                                    refs_heads_dfd, ".",
                                    ret_all_refs, cancellable, error))
         return FALSE;
@@ -624,7 +641,7 @@ _ostree_repo_list_refs_internal (OstreeRepo       *self,
           if (!glnx_opendirat (dfd_iter.fd, dent->d_name, TRUE, &remote_dfd, error))
             return FALSE;
 
-          if (!enumerate_refs_recurse (self, dent->d_name, NULL, remote_dfd, base_path,
+          if (!enumerate_refs_recurse (self, dent->d_name, flags, NULL, remote_dfd, base_path,
                                        remote_dfd, ".",
                                        ret_all_refs,
                                        cancellable, error))
@@ -655,7 +672,10 @@ ostree_repo_list_refs (OstreeRepo       *self,
                        GCancellable     *cancellable,
                        GError          **error)
 {
-  return _ostree_repo_list_refs_internal (self, TRUE, refspec_prefix, out_all_refs, cancellable, error);
+  return _ostree_repo_list_refs_internal (self, TRUE,
+                                          OSTREE_REPO_LIST_REFS_EXT_NONE,
+                                          refspec_prefix, out_all_refs,
+                                          cancellable, error);
 }
 
 /**
@@ -681,7 +701,9 @@ ostree_repo_list_refs_ext (OstreeRepo                 *self,
                            GCancellable               *cancellable,
                            GError                     **error)
 {
-  return _ostree_repo_list_refs_internal (self, FALSE, refspec_prefix, out_all_refs, cancellable, error);
+  return _ostree_repo_list_refs_internal (self, FALSE, flags,
+                                          refspec_prefix, out_all_refs,
+                                          cancellable, error);
 }
 
 /**
@@ -757,17 +779,43 @@ ostree_repo_remote_list_refs (OstreeRepo       *self,
   return TRUE;
 }
 
+static char *
+relative_symlink_to (const char *relpath,
+                     const char *target)
+{
+  g_assert (*relpath);
+  g_assert (*target && *target != '/');
+
+  g_autoptr(GString) buf = g_string_new ("");
+
+  while (TRUE)
+    {
+      const char *slash = strchr (relpath, '/');
+      if (!slash)
+        break;
+      relpath = slash + 1;
+      g_string_append (buf, "../");
+    }
+
+  g_string_append (buf, target);
+
+  return g_string_free (g_steal_pointer (&buf), FALSE);
+}
+
+/* May specify @rev or @alias */
 gboolean
 _ostree_repo_write_ref (OstreeRepo                 *self,
                         const char                 *remote,
                         const OstreeCollectionRef  *ref,
                         const char                 *rev,
+                        const char                 *alias,
                         GCancellable               *cancellable,
                         GError                    **error)
 {
   glnx_fd_close int dfd = -1;
 
   g_return_val_if_fail (remote == NULL || ref->collection_id == NULL, FALSE);
+  g_return_val_if_fail (!(rev != NULL && alias != NULL), FALSE);
 
   if (remote != NULL && !ostree_validate_remote_name (remote, error))
     return FALSE;
@@ -832,7 +880,7 @@ _ostree_repo_write_ref (OstreeRepo                 *self,
         return glnx_throw_errno_prefix (error, "Opening remotes/ dir %s", remote);
     }
 
-  if (rev == NULL)
+  if (rev == NULL && alias == NULL)
     {
       if (dfd >= 0)
         {
@@ -843,11 +891,32 @@ _ostree_repo_write_ref (OstreeRepo                 *self,
             }
         }
     }
-  else
+  else if (rev != NULL)
     {
       if (!write_checksum_file_at (self, dfd, ref->ref_name, rev, cancellable, error))
         return FALSE;
     }
+  else if (alias != NULL)
+    {
+      const char *lastslash = strrchr (ref->ref_name, '/');
+
+      if (lastslash)
+        {
+          char *parent = strdupa (ref->ref_name);
+          parent[lastslash - ref->ref_name] = '\0';
+
+          if (!glnx_shutil_mkdir_p_at (dfd, parent, 0755, cancellable, error))
+            return FALSE;
+        }
+
+      g_autofree char *reltarget = relative_symlink_to (ref->ref_name, alias);
+      g_autofree char *tmplink = NULL;
+      if (!_ostree_make_temporary_symlink_at (self->tmp_dir_fd, reltarget,
+                                              &tmplink, cancellable, error))
+        return FALSE;
+      if (!glnx_renameat (self->tmp_dir_fd, tmplink, dfd, ref->ref_name, error))
+        return FALSE;
+    }
 
   if (!_ostree_repo_update_mtime (self, error))
     return FALSE;
@@ -876,7 +945,7 @@ _ostree_repo_update_refs (OstreeRepo        *self,
         return FALSE;
 
       const OstreeCollectionRef ref = { NULL, ref_name };
-      if (!_ostree_repo_write_ref (self, remote, &ref, rev,
+      if (!_ostree_repo_write_ref (self, remote, &ref, rev, NULL,
                                    cancellable, error))
         return FALSE;
     }
@@ -899,7 +968,7 @@ _ostree_repo_update_collection_refs (OstreeRepo        *self,
       const OstreeCollectionRef *ref = key;
       const char *rev = value;
 
-      if (!_ostree_repo_write_ref (self, NULL, ref, rev,
+      if (!_ostree_repo_write_ref (self, NULL, ref, rev, NULL,
                                    cancellable, error))
         return FALSE;
     }
@@ -961,7 +1030,8 @@ ostree_repo_list_collection_refs (OstreeRepo    *self,
       if (!glnx_opendirat (self->repo_dir_fd, "refs/heads", TRUE, &refs_heads_dfd, error))
         return FALSE;
 
-      if (!enumerate_refs_recurse (self, NULL, main_collection_id, refs_heads_dfd, base_path,
+      if (!enumerate_refs_recurse (self, NULL, OSTREE_REPO_LIST_REFS_EXT_NONE,
+                                   main_collection_id, refs_heads_dfd, base_path,
                                    refs_heads_dfd, ".",
                                    ret_all_refs, cancellable, error))
         return FALSE;
@@ -993,7 +1063,8 @@ ostree_repo_list_collection_refs (OstreeRepo    *self,
       if (!glnx_opendirat (dfd_iter.fd, dent->d_name, TRUE, &collection_dfd, error))
         return FALSE;
 
-      if (!enumerate_refs_recurse (self, NULL, dent->d_name, collection_dfd, base_path,
+      if (!enumerate_refs_recurse (self, NULL, OSTREE_REPO_LIST_REFS_EXT_NONE,
+                                   dent->d_name, collection_dfd, base_path,
                                    collection_dfd, ".",
                                    ret_all_refs,
                                    cancellable, error))
index f1e964b7bc29fac2c8221b5315743f3897de871c..decf9a4e4e01bc538d826ec532ee0a027b57967c 100644 (file)
@@ -327,6 +327,14 @@ gboolean      ostree_repo_set_ref_immediate (OstreeRepo *self,
                                              GCancellable  *cancellable,
                                              GError       **error);
 
+_OSTREE_PUBLIC
+gboolean      ostree_repo_set_alias_ref_immediate (OstreeRepo *self,
+                                                   const char *remote,
+                                                   const char *ref,
+                                                   const char *target,
+                                                   GCancellable  *cancellable,
+                                                   GError       **error);
+
 #ifdef OSTREE_ENABLE_EXPERIMENTAL_API
 
 _OSTREE_PUBLIC
@@ -452,9 +460,11 @@ gboolean      ostree_repo_list_refs (OstreeRepo       *self,
 /**
  * OstreeRepoListRefsExtFlags:
  * @OSTREE_REPO_LIST_REFS_EXT_NONE: No flags.
+ * @OSTREE_REPO_LIST_REFS_EXT_ALIASES: Only list aliases.  Since: 2017.10
  */
 typedef enum {
   OSTREE_REPO_LIST_REFS_EXT_NONE = 0,
+  OSTREE_REPO_LIST_REFS_EXT_ALIASES = 1,
 } OstreeRepoListRefsExtFlags;
 
 _OSTREE_PUBLIC
index f2274802281ceee8b8e02e647e8acfa849ad5abd..0f850069659f6b58503916c72ce7ccbcad71c94d 100644 (file)
@@ -28,6 +28,7 @@
 
 static gboolean opt_delete;
 static gboolean opt_list;
+static gboolean opt_alias;
 static char *opt_create;
 #ifdef OSTREE_ENABLE_EXPERIMENTAL_API
 static gboolean opt_collections;
@@ -36,6 +37,7 @@ static gboolean opt_collections;
 static GOptionEntry options[] = {
   { "delete", 0, 0, G_OPTION_ARG_NONE, &opt_delete, "Delete refs which match PREFIX, rather than listing them", NULL },
   { "list", 0, 0, G_OPTION_ARG_NONE, &opt_list, "Do not remove the prefix from the refs", NULL },
+  { "alias", 'A', 0, G_OPTION_ARG_NONE, &opt_alias, "If used with --create, create an alias, otherwise just list aliases", NULL },
   { "create", 0, 0, G_OPTION_ARG_STRING, &opt_create, "Create a new ref for an existing commit", "NEWREF" },
 #ifdef OSTREE_ENABLE_EXPERIMENTAL_API
   { "collections", 'c', 0, G_OPTION_ARG_NONE, &opt_collections, "Enable listing collection IDs for refs", NULL },
@@ -132,18 +134,36 @@ do_ref_with_collections (OstreeRepo    *repo,
 static gboolean do_ref (OstreeRepo *repo, const char *refspec_prefix, GCancellable *cancellable, GError **error)
 {
   g_autoptr(GHashTable) refs = NULL;
+  g_autoptr(GHashTable) ref_aliases = NULL;
   GHashTableIter hashiter;
   gpointer hashkey, hashvalue;
   gboolean ret = FALSE;
+  gboolean is_list;
 
 #ifdef OSTREE_ENABLE_EXPERIMENTAL_API
   if (opt_collections)
     return do_ref_with_collections (repo, refspec_prefix, cancellable, error);
 #endif  /* OSTREE_ENABLE_EXPERIMENTAL_API */
 
-  if (opt_delete || opt_list)
+  /* If we're doing aliasing, we need the full list of aliases mostly to allow
+   * replacing existing aliases.
+   */
+  if (opt_alias)
     {
-      if (!ostree_repo_list_refs_ext (repo, refspec_prefix, &refs, OSTREE_REPO_LIST_REFS_EXT_NONE,
+      if (!ostree_repo_list_refs_ext (repo, NULL, &ref_aliases,
+                                      OSTREE_REPO_LIST_REFS_EXT_ALIASES,
+                                      cancellable, error))
+        goto out;
+    }
+
+  is_list = !(opt_delete || opt_create);
+
+  if (opt_delete || opt_list || (!opt_create && opt_alias))
+    {
+      OstreeRepoListRefsExtFlags flags = OSTREE_REPO_LIST_REFS_EXT_NONE;
+      if (opt_alias)
+        flags |= OSTREE_REPO_LIST_REFS_EXT_ALIASES;
+      if (!ostree_repo_list_refs_ext (repo, refspec_prefix, &refs, flags,
                                       cancellable, error))
         goto out;
     }
@@ -156,13 +176,14 @@ static gboolean do_ref (OstreeRepo *repo, const char *refspec_prefix, GCancellab
   else if (!ostree_repo_list_refs (repo, refspec_prefix, &refs, cancellable, error))
     goto out;
 
-  if (!opt_delete && !opt_create)
+  if (is_list)
     {
-      g_hash_table_iter_init (&hashiter, refs);
-      while (g_hash_table_iter_next (&hashiter, &hashkey, &hashvalue))
+      GLNX_HASH_TABLE_FOREACH_KV (refs, const char *, ref, const char *, value)
         {
-          const char *ref = hashkey;
-          g_print ("%s\n", ref);
+          if (opt_alias)
+            g_print ("%s -> %s\n", ref, value);
+          else
+            g_print ("%s\n", ref);
         }
     }
   else if (opt_create)
@@ -183,22 +204,33 @@ static gboolean do_ref (OstreeRepo *repo, const char *refspec_prefix, GCancellab
           else goto out;
         }
 
-      if (checksum_existing != NULL)
+      /* We want to allow replacing an existing alias */
+      gboolean replacing_alias = opt_alias && g_hash_table_contains (ref_aliases, opt_create);
+      if (!replacing_alias && checksum_existing != NULL)
         {
           g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                        "--create specified but ref %s already exists", opt_create);
           goto out;
         }
 
-      if (!ostree_repo_resolve_rev (repo, refspec_prefix, FALSE, &checksum, error))
-        goto out;
-
       if (!ostree_parse_refspec (opt_create, &remote, &ref, error))
         goto out;
 
-      if (!ostree_repo_set_ref_immediate (repo, remote, ref, checksum,
-                                          cancellable, error))
-        goto out;
+      if (opt_alias)
+        {
+          if (!ostree_repo_set_alias_ref_immediate (repo, remote, ref, refspec_prefix,
+                                                    cancellable, error))
+            goto out;
+        }
+      else
+        {
+          if (!ostree_repo_resolve_rev (repo, refspec_prefix, FALSE, &checksum, error))
+            goto out;
+
+          if (!ostree_repo_set_ref_immediate (repo, remote, ref, checksum,
+                                              cancellable, error))
+            goto out;
+        }
     }
   else
     /* delete */
index d4db001386a0576c7ff881dfaa4188379d626797..e48784aade64a22debf91a47f2d9011e5917e8b8 100755 (executable)
@@ -23,7 +23,7 @@ set -euo pipefail
 
 setup_fake_remote_repo1 "archive-z2"
 
-echo '1..1'
+echo '1..2'
 
 cd ${test_tmpdir}
 mkdir repo
@@ -117,3 +117,43 @@ ${CMD_PREFIX} ostree --repo=repo refs | wc -l > refscount.create6
 assert_file_has_content refscount.create6 "^11$"
 
 echo "ok refs"
+
+# Test symlinking a ref
+${CMD_PREFIX} ostree --repo=repo refs ctest --create=exampleos/x86_64/26/server
+${CMD_PREFIX} ostree --repo=repo refs -A exampleos/x86_64/26/server --create=exampleos/x86_64/stable/server
+${CMD_PREFIX} ostree --repo=repo summary -u
+${CMD_PREFIX} ostree --repo=repo refs > refs.txt
+for v in 26 stable; do
+    assert_file_has_content refs.txt exampleos/x86_64/${v}/server
+done
+${CMD_PREFIX} ostree --repo=repo refs -A > refs.txt
+assert_file_has_content_literal refs.txt 'exampleos/x86_64/stable/server -> exampleos/x86_64/26/server'
+assert_not_file_has_content refs.txt '^exampleos/x86_64/26/server'
+stable=$(${CMD_PREFIX} ostree --repo=repo rev-parse exampleos/x86_64/stable/server)
+current=$(${CMD_PREFIX} ostree --repo=repo rev-parse exampleos/x86_64/26/server)
+assert_streq "${stable}" "${current}"
+${CMD_PREFIX} ostree --repo=repo commit -b exampleos/x86_64/26/server --tree=dir=tree
+${CMD_PREFIX} ostree --repo=repo summary -u
+newcurrent=$(${CMD_PREFIX} ostree --repo=repo rev-parse exampleos/x86_64/26/server)
+assert_not_streq "${newcurrent}" "${current}"
+newstable=$(${CMD_PREFIX} ostree --repo=repo rev-parse exampleos/x86_64/stable/server)
+assert_streq "${newcurrent}" "${newstable}"
+
+# Test that we can swap the symlink
+${CMD_PREFIX} ostree --repo=repo commit -b exampleos/x86_64/27/server --tree=dir=tree
+newcurrent=$(${CMD_PREFIX} ostree --repo=repo rev-parse exampleos/x86_64/27/server)
+assert_not_streq "${newcurrent}" "${newstable}"
+${CMD_PREFIX} ostree --repo=repo refs -A exampleos/x86_64/27/server --create=exampleos/x86_64/stable/server
+newnewstable=$(${CMD_PREFIX} ostree --repo=repo rev-parse exampleos/x86_64/stable/server)
+assert_not_streq "${newnewstable}" "${newstable}"
+assert_streq "${newnewstable}" "${newcurrent}"
+${CMD_PREFIX} ostree --repo=repo refs > refs.txt
+for v in 26 27 stable; do
+    assert_file_has_content refs.txt exampleos/x86_64/${v}/server
+done
+${CMD_PREFIX} ostree --repo=repo refs -A > refs.txt
+assert_file_has_content_literal refs.txt 'exampleos/x86_64/stable/server -> exampleos/x86_64/27/server'
+
+${CMD_PREFIX} ostree --repo=repo summary -u
+
+echo "ok ref symlink"