You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@subversion.apache.org by ri...@apache.org on 2026/04/01 17:44:08 UTC

svn commit: r1932708 - subversion/trunk/subversion/svnbrowse

Author: rinrab
Date: Wed Apr  1 17:44:08 2026
New Revision: 1932708

Log:
svnbrowse: Implement the most basic functionality of the directory browser. 

- Simple command-line parser that takes the first argument processes it as the
  URL to operate on.
- Initialize the client context with pre-set authentication handlers. Use the
  standard command line handlers for that.
- Retrieving the directory contents and display them.
- Allow some control to navigate to subdirs.

* subversion/svnbrowse/svnbrowse.c
  (includes): Include required headers.
  (item_t): Declare struct to hold information about a single directory.
  (svn_browse__ctx_t): Declare struct to hold context of the browser.
  (init_client, list_cb, enter_path): New functions.
  (ui_draw): Render the list.
  (sub_main): Add initialization and handle browser commands.

Modified:
   subversion/trunk/subversion/svnbrowse/svnbrowse.c

Modified: subversion/trunk/subversion/svnbrowse/svnbrowse.c
==============================================================================
--- subversion/trunk/subversion/svnbrowse/svnbrowse.c	Wed Apr  1 17:21:18 2026	(r1932707)
+++ subversion/trunk/subversion/svnbrowse/svnbrowse.c	Wed Apr  1 17:44:08 2026	(r1932708)
@@ -24,6 +24,9 @@
 
 #include <apr.h>
 
+#include "svn_client.h"
+#include "svn_opt.h"
+#include "svn_path.h"
 #include "svn_pools.h"
 #include "svn_cmdline.h"
 #include "svn_error.h"
@@ -34,15 +37,124 @@
  * 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;
+
+  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));
+
+  /* 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 void
-ui_draw(apr_pool_t *pool)
+ui_draw(svn_browse__ctx_t *ctx, apr_pool_t *pool)
 {
-  mvprintw(0, 4, "svnbrowse: work in progress");
+  int i;
+
+  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();
+    }
 }
 
 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();
 
@@ -56,7 +168,7 @@ sub_main(int *code, int argc, char *argv
       int ch;
 
       clear();
-      ui_draw(pool);
+      ui_draw(&ctx, pool);
       refresh();
 
       ch = getch();
@@ -71,9 +183,28 @@ sub_main(int *code, int argc, char *argv
        * 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. */
-      if (ch == 'q')
+      else if (ch == 'q')
         {
           break;
         }


Re: svn commit: r1932708 - subversion/trunk/subversion/svnbrowse

Posted by Timofei Zhakov <ti...@chemodax.net>.
On Mon, Apr 6, 2026 at 12:22 PM Daniel Sahlberg <da...@gmail.com>
wrote:

> Hi,
>
> I took a look at svnbrowse today and it is coming together nicely! Of
> course I had to try to break it as well... I suppose you are already aware
> but it would make life easier when testing if we could sort this out.
>

Thanks for testing, nice!


> I picked a random commit trying to explain.
>
> Den ons 1 apr. 2026 kl 22:53 skrev <ri...@apache.org>:
>
> ...
>
>
>>
>>  static svn_error_t *
>>  sub_main(int *code, int argc, char *argv[], apr_pool_t *pool)
>>  {
>>
>
> ...sub_main initializes the screen...
>
>
>>    /* init the display */
>>    initscr();
>>
>>
> Then later on when processing some keypresses, we call SVN_ERR()
>
>
>> +      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));
>> +        }
>>
>
> SVN_ERR return whenever there is an error. This leads to
>
> quit:
>     endwin();
>
> never being executed, thus leaving the terminal in an unusable state.
> (`reset` works fine to restore it).
>
> I initially thought about a global variable and moving endwin() to the
> proper main(), but thinking twice it might be better to just move the
> keypress processing loop to its own function.
>
> Thoughts?
>
>
Yes, this problem exists. Moving keypress handling to a separate function
does make sense but then it's not so obvious to make cancellation (by
q/esc). We could throw an error, but the question is which one. Perhaps
SVN_ERR_CANCELLED but it could be produced by libsvn as well.

The issue with doing all of that in main is that curses methods could
result in an error. We don't handle any of these yet, but technically
should.

$ man endwin
[[[
...
•   endwin returns ERR if
         •   the terminal was not initialized,
         •   it is called more than once without updating the screen, or
         •   its call of reset_shell_mode(3X) returns ERR; and
...
]]]

Which we would like to handle with svn_error in future.

Also we can always replace SVN_ERR() use with manual checks to safely quit
with an error.

I think we should also set up handlers for abort
(svn_error_set_malfunction_handler) to reset terminal state and signal
handling (CTRL+C).

By the way, did it crash by entering a file? Because I don't know yet what
the behaviour should be. Obviously we don't want to crash. Also we receive
node type when listing a directory so we could almost certainly tell what
node type it will be. However, we can't because it can change since we
listed it to the moment we entered this node.

I suggest we first assume that it didn't change, and in this rare occasion,
if that operation created an error, our program can change its mind and try
to fetch it as a different type. At the moment we refetch it, it may change
its type back.

However, the same problem would happen if the node was removed entirely.

-- 
Timofei Zhakov

Re: svn commit: r1932708 - subversion/trunk/subversion/svnbrowse

Posted by Daniel Sahlberg <da...@gmail.com>.
Hi,

I took a look at svnbrowse today and it is coming together nicely! Of
course I had to try to break it as well... I suppose you are already aware
but it would make life easier when testing if we could sort this out.

I picked a random commit trying to explain.

Den ons 1 apr. 2026 kl 22:53 skrev <ri...@apache.org>:

...


>
>  static svn_error_t *
>  sub_main(int *code, int argc, char *argv[], apr_pool_t *pool)
>  {
>

...sub_main initializes the screen...


>    /* init the display */
>    initscr();
>
>
Then later on when processing some keypresses, we call SVN_ERR()


> +      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));
> +        }
>

SVN_ERR return whenever there is an error. This leads to

quit:
    endwin();

never being executed, thus leaving the terminal in an unusable state.
(`reset` works fine to restore it).

I initially thought about a global variable and moving endwin() to the
proper main(), but thinking twice it might be better to just move the
keypress processing loop to its own function.

Thoughts?

/Daniel