Hello all,

The problem I would like to address is that actions like picking the right
branch in a repository are sometimes annoying with the current UI of the
command-line. Although all operations are really well-designed, the user still
needs to manually input the whole URL of a branch/or use the relative path
syntax.

There is not enough user feedback. When interacting with a repository through
the CLI it feels like some abstract thing that exists somewhere on the remote
target - not a file-system tree. The current way we usually do that is one of
the following:

1. Imagine what we have on the server in our minds. It's often not that big of
   a deal to type 30 characters when switching/merging stuff.

2. Use the web interface (if any).

3. Use third-party tools like TortoiseSVN Repository Browser (and the whole
   ecosystem including branch picker in switch/merge which I believe is almost
   the same thing).

4. Borrow the right command with the exact path from another resource (like
   when first time checking out a new project).

The 2 and 3 are not always possible as the standard web interface is very
limited in terms of functionality and not always do we have the pleasure to use
the GUI apps.

What I believe we need to improve overall workflow with Subversion is a way to
browse repositories (without checking it out) directly in a terminal. Luckily
because of the way accessing remote targets is designed in Subversion, it's
possible to retrieve information of any arbitrary node without a need to fetch
it entirely.

I would like to propose introducing a tool for browsing remote repositories
(svnbrowse). It will be a TUI (terminal user interface) like-ish application
where a user could navigate the repository like in a web browser.

I have tried to implement it. A patch is attached below. I generally liked the
user experience it brings.

There are also a few issues we might face when implementing this feature;

1. It currently loads items pretty slowly; Initially I used the svn_client API.
   However, it creates a new ra_session per each call. I believe it would
   be better to switch to using svn_ra directly.

2. We might load the tree recursively for faster navigation between
   directories. This would also allow fuzzy searching. But it makes the
   operation unbounded.

3. Should it work over a working copy or it's a web browser replacement? Using
   URL from a working copy makes it much more convenient to use as a user only
   needs to type 'svnbrowse' to get into it.

4. The revision issue; What revision do we use? If implementing it like in the
   rest of the commands (with --revision that defaults to HEAD), how often
   should we resolve it? The RA API (and the protocol) also allows fetching the
   contents of the HEAD directory (using svn_ra_get_dir2 with
   SVN_INVALID_REVNUM revision). However, there is no way to get the revnum
   back (without making an extra request).

5. Should it be a separate program or something like an option in
   'svn list --please-let-me-browse-it'. I personally think that it should not
   be in 'svn' command. By conceptual conventions of 'svn' there are minimal
   interactions and it can be used for scripting as well. I believe it would be
   much better to separate it into a different program.

6. I suggest limiting the scope to directory browsing as it's the simplest to
   implement but it improves the experience by a lot. Later on, adding file
   content browsing and log would be natural. Also it may act as an
   alternative to svnmucc if a commit operation was implemented.

7. Do we use ncurses (library that the majority of TUI apps use) or figure out
   something else?

This list is not complete and I may have missed something; To conclude, there
are plenty of things to be done and many problems with on obvious solution.
Better we try something out and get some feedback and vision of what is to be
improved. The prototype represents the general wireframe of what it should
like. I made it in like an hour to get an overall impression.

Please feel free to express your opinion about this idea. Dear svndev, it's
time to discuss some UI things >-<

-- 
Timofei Zhakov
Index: CMakeLists.txt
===================================================================
--- CMakeLists.txt      (revision 1932510)
+++ CMakeLists.txt      (working copy)
@@ -483,6 +483,17 @@ if(SVN_ENABLE_AUTH_GNOME_KEYRING)
   )
 endif()
 
+if (TRUE)
+  add_library(external-ncurses INTERFACE)
+
+  if(SVN_USE_PKG_CONFIG)
+    pkg_check_modules(ncurses REQUIRED IMPORTED_TARGET ncurses)
+    target_link_libraries(external-ncurses INTERFACE PkgConfig::ncurses)
+  else()
+    message(ERROR "not supported")
+  endif()
+endif()
+
 if(SVN_ENABLE_SWIG_PERL OR SVN_ENABLE_SWIG_PYTHON OR SVN_ENABLE_SWIG_RUBY)
   find_package(SWIG REQUIRED)
   include(${SWIG_USE_FILE})
Index: build.conf
===================================================================
--- build.conf  (revision 1932510)
+++ build.conf  (working copy)
@@ -222,6 +222,12 @@ libs = libsvn_client libsvn_ra libsvn_subr libsvn_
 install = bin
 manpages = subversion/svnmucc/svnmucc.1
 
+[svnbrowse]
+type = exe
+path = subversion/svnbrowse
+libs = libsvn_client libsvn_ra libsvn_subr libsvn_delta ncurses
+install = bin
+
 # Support for GNOME Keyring
 [libsvn_auth_gnome_keyring]
 description = Subversion GNOME Keyring Library
@@ -1505,6 +1511,10 @@ external-lib = $(SVN_SASL_LIBS)
 type = lib
 external-lib = $(SVN_OPENSSL_LIBS) $(SVN_LIBCRYPTO_LIBS)
 
+[ncurses]
+type = lib
+external-lib = $(SVN_NCURSES_LIBS)
+
 [checksum-libs]
 type = lib
 external-lib = $(SVN_CHECKSUM_LIBS)
Index: subversion/svnbrowse/svnbrowse.c
===================================================================
--- subversion/svnbrowse/svnbrowse.c    (nonexistent)
+++ subversion/svnbrowse/svnbrowse.c    (working copy)
@@ -0,0 +1,318 @@
+#include <apr.h>
+
+#include "svn_client.h"
+#include "svn_pools.h"
+#include "svn_cmdline.h"
+#include "svn_error.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+
+#include <ncurses.h>
+
+/*
+ * This a TUI (terminal user interface) client for browsing and exploring
+ * remote Subversion targets.
+ *
+ * We currently use ncurses to render stuff. However, there is a way to
+ * implement interactions with the terminal directly. It's not that complicated
+ * considering that modern terminal emulators support escape sequences (which
+ * can be used to teleport cursor, change colours & fonts styles, and more).
+ * Also I imagine it would be pain to restore terminal content when closing
+ * svnbrowse.
+ *
+ * Reasons to use ncurses:
+ *   - cross platform
+ *   - available on the majority of systems
+ * Reasons to do something else:
+ *   - could be missing on some systems
+ *   - extra dependency
+ *   - old and [cursed]
+ *
+ * By the way it's not a GPL software. ncurses is published under the MIT/X11
+ * license which is a permissive license.
+ *
+ * All interactions with the repository are currently peroformed in readonly
+ * mode. The main focus is to browse files. Although there is a potential room
+ * to implement some action that modify contents like delete/copy/move/mkdir.
+ *
+ * It would be nice to support displaying file contents in the same
+ * application. However this is quiet problematic. Potential ways to go would
+ * be to launch $SVN_EDITOR, but it's not guaranteed to exist. Or reinvent
+ * /bin/less.
+ *
+ * The current implementation relies on libsvn_client API. However, there are
+ * certain limitations of it and perhaps it's actually worth it to directly use
+ * the libsvn_ra instead. This way we save the need for extra handshakes when
+ * establishing connection over the wire (which happens with svn_client even
+ * when re-using the same context). It is also an extra abstraction which this
+ * application does not really use - I believe low-level control would be more
+ * important in this case.
+ *
+ * This application is intendent to be similar to vim's :Explore file browser
+ * (also known as Netrw). It's pretty minimal, simple, and [intuitive?].
+ * Another way to say it's a command-line replacement for web UIs. Or
+ * TortoiseSVN's RepositoryBrowser for terminal/and is cross-platform.
+ */
+
+/*
+ * TODO list:
+ * - Don't leak memory (would be really nice).
+ * - Avoid SEGFAULT-ing when entering selection from the outside of the list.
+ * - Resolve revision and list contents via separate steps.
+ * - Eliminate ncurses (?)
+ * - Sorting.
+ * - Hide cursor.
+ * - Support refreshing.
+ * - Scroller.
+ * - Better command-line argument processing (authentication, config, revision)
+ * - Implement some way to jump between different revisions without exiting.
+ * - List repositories on remote server (there is no canonical interface to do
+ *   that).
+ * - Support path to working copy on local filesystem (Should it use the
+ *   revision of the node in the WC or go straigh to HEAD? It's sometimes quiet
+ *   annoying when the commands like 'svn log' refuse to fallback to head even
+ *   when "I obviously wanted to log everything since the recent change". On
+ *   the other hand this behavious is technically correct. This also puts
+ *   svnbrowse to libsvn_client level so it can access the WC.)
+ * - Select active item in the list on the full screen.
+ * - Snapshots for better updates.
+ * - Cache directory contents to potentially make going up a directory instant.
+ *   Or even reopening known directories. But this could be problematic.
+ * - Search. Perhaps need preloading the entire tree recursively which is
+ *   suboptimal. On the other hand, it allows for instant navigation after it's
+ *   preloaded.
+ * - libsvn_client/libsvn_ra
+ * - Implement opening file contents ($SVN_EDITOR or built-in viewer?).
+ * - svn_browse__dir_t to hold whole directory so that it can be free-d
+ *   applicatoin can compose them as much as they want (like maintain a stack
+ *   to get upper directory from cache instead of fetching it from the server
+ *   each time.
+ */
+
+/* Control+ASCII character are represented as values 1-26 according to their
+ * alphabetical order. */
+#define CTRL(ch) (ch - 'a' + 1)
+
+typedef struct item_t {
+  const char *relpath;
+  const svn_dirent_t *dirent;
+} item_t;
+
+typedef struct svn_browse__ctx_t {
+  const char *root;
+  const char *relpath;
+  const char *abspath;
+  svn_opt_revision_t revision;
+
+  svn_client_ctx_t *client;
+  svn_auth_baton_t *auth;
+
+  apr_array_header_t *list;
+  int selection;
+  apr_pool_t *list_pool;
+} svn_browse__ctx_t;
+
+static svn_error_t *
+init_client(svn_browse__ctx_t *ctx, apr_pool_t *pool)
+{
+  svn_auth_baton_t *auth;
+
+  SVN_ERR(svn_client_create_context2(&ctx->client, NULL, pool));
+
+#if 0
+  /* Set up our cancellation support. */
+  svn_cl__check_cancel = svn_cmdline__setup_cancellation_handler();
+  ctx->cancel_func = svn_cl__check_cancel;
+#endif
+
+  /* Set up Authentication stuff. */
+  SVN_ERR(svn_cmdline_create_auth_baton2(
+            &auth,
+            FALSE,
+            NULL,
+            NULL,
+            NULL,
+            FALSE,
+            FALSE,
+            FALSE,
+            FALSE,
+            FALSE,
+            FALSE,
+            NULL,
+            NULL,
+            NULL,
+            pool));
+
+  ctx->client->auth_baton = auth;
+
+  return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+list_cb(void *baton,
+        const char *path,
+        const svn_dirent_t *dirent,
+        const svn_lock_t *lock,
+        const char *abs_path,
+        const char *external_parent_url,
+        const char *external_target,
+        apr_pool_t *scratch_pool)
+{
+  svn_browse__ctx_t *ctx = baton;
+  item_t *item = apr_pcalloc(ctx->list_pool, sizeof(*item));
+  item->relpath = apr_pstrdup(ctx->list_pool, path);
+  item->dirent = svn_dirent_dup(dirent, ctx->list_pool);
+  APR_ARRAY_PUSH(ctx->list, item_t *) = item;
+  return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+enter_path(svn_browse__ctx_t *ctx, const char *relpath, apr_pool_t *pool)
+{
+  ctx->relpath = apr_pstrdup(pool, relpath);
+  ctx->abspath = svn_path_url_add_component2(ctx->root, relpath, pool);
+
+  ctx->list = apr_array_make(pool, 0, sizeof(item_t *));
+  ctx->selection = 0;
+
+  SVN_ERR(svn_client_list4(ctx->abspath, &ctx->revision, &ctx->revision, NULL,
+                           svn_depth_immediates, SVN_DIRENT_ALL, TRUE, TRUE,
+                           list_cb, ctx, ctx->client, pool));
+
+  return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+ui_draw(svn_browse__ctx_t *ctx, apr_pool_t *pool)
+{
+  int i;
+
+  /* please note: x and y are swapped in mvprintw() */
+
+  mvprintw(0, 4, "Browsing: %s", ctx->abspath);
+
+  for (i = 0; i < ctx->list->nelts; i++)
+    {
+      item_t *item = APR_ARRAY_IDX(ctx->list, i, item_t *);
+
+      if (i == ctx->selection)
+        standout();
+
+      if (i == 0)
+        mvprintw(i + 1, 0, "../");
+      else if (item->dirent->kind == svn_node_dir)
+        mvprintw(i + 1, 0, "%s/", item->relpath);
+      else if (item->dirent->kind == svn_node_file)
+        mvprintw(i + 1, 0, "%s", item->relpath);
+      else
+        abort();
+
+                       mvprintw(i + 1, COLS - 40, "%8ld KiB  r%-8ld  %s",
+               item->dirent->size / 1024,
+               item->dirent->created_rev,
+               item->dirent->last_author);
+
+      if (i == ctx->selection)
+        standend();
+    }
+
+  return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+sub_main(int *code, int argc, char *argv[], apr_pool_t *pool)
+{
+  svn_browse__ctx_t ctx = { 0 };
+
+  if (argc != 2)
+    return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, "usage: 
svnbrowse <URL>");
+
+  SVN_ERR(svn_uri_canonicalize_safe(&ctx.root, NULL, argv[1], pool, pool));
+  ctx.revision.kind = svn_opt_revision_head;
+  ctx.list_pool = pool;
+
+  SVN_ERR(init_client(&ctx, pool));
+  SVN_ERR(enter_path(&ctx, "", pool));
+
+  /* init the display */
+  initscr();
+
+  /* put ncurses into keypad mode to handle arrow inputs */
+  intrflush(stdscr, FALSE);
+  keypad(stdscr, TRUE);
+  nonl();
+
+  while (TRUE)
+  {
+    int ch;
+
+    clear();
+    SVN_ERR(ui_draw(&ctx, pool));
+    refresh();
+
+    ch = getch();
+
+    /* getch() reads the next character/key with the following additional
+     * rules:
+     * 1. as we configured it to use keypad(), arrows and other special keys
+     *    are handled as KEY_XXX.
+     * 2. Control (CTRL) version are handled as literal 1-26 values of ch where
+     *    1 is <C-A> and 26 is <C-Z>.
+     * 3. The rest of keys remain as their equivalents on the current layout.
+     * 4. If shift is held, they just become uppercased.
+     */
+
+    if (ch == KEY_UP || ch == 'k')
+      {
+        ctx.selection--;
+      }
+    else if (ch == KEY_DOWN || ch == 'j')
+      {
+        ctx.selection++;
+      }
+    else if (ch == '\n' || ch == '\r')
+      {
+        item_t *item = APR_ARRAY_IDX(ctx.list, ctx.selection, item_t *);
+        const char *new_url = svn_relpath_join(ctx.relpath, item->relpath, 
pool);
+        SVN_ERR(enter_path(&ctx, new_url, pool));
+      }
+    else if (ch == KEY_BACKSPACE || ch == '-' || ch == 'u')
+      {
+        const char *new_url = svn_relpath_dirname(ctx.relpath, pool);
+        SVN_ERR(enter_path(&ctx, new_url, pool));
+      }
+    /* TODO: quit via escape. some say just check for 27, but it I think it's
+     * a bit ugly. */
+    else if (ch == 'q')
+      {
+        break;
+      }
+  }
+
+       endwin();
+
+  return SVN_NO_ERROR;
+}
+
+int main(int argc, char *argv[])
+{
+  apr_pool_t *pool;
+  int exit_code = EXIT_SUCCESS;
+  svn_error_t *err;
+
+  if (svn_cmdline_init("svnbrowse", stderr) != EXIT_SUCCESS)
+    return EXIT_FAILURE;
+
+  pool = apr_allocator_owner_get(svn_pool_create_allocator(FALSE));
+
+  err = sub_main(&exit_code, argc, argv, pool);
+
+  if (err)
+    {
+      exit_code = EXIT_FAILURE;
+      svn_cmdline_handle_exit_error(err, NULL, "svnbrowse: ");
+    }
+
+  svn_pool_destroy(pool);
+  return exit_code;
+}

Property changes on: subversion/svnbrowse/svnbrowse.c
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property

Reply via email to