# HG changeset patch
# User Ry4an Brase <ry4an-hg@ry4an.org>
# Date 2008-11-03 09:20:28
# Node ID c21d236ca897105057dbf8d5609057eaa6f28088
# Parent  68374f1c8c87177dec40de56f3b046a84693e9be

hgweb: descend empty directories in web view

When a manifest has a series of directories with nothing in them but a single
directory, displaying the entire chain of empty directories allows for
navigation down to the first non-empty directory with a single click.

Because Java links package hierarchy to directory hierarchy, and because Java
conventions include at least three empty directories at the top of this
hierarchy, descending down empty directories is very common in Java web tools.

diff --git a/mercurial/hgweb/webcommands.py b/mercurial/hgweb/webcommands.py
--- a/mercurial/hgweb/webcommands.py
+++ b/mercurial/hgweb/webcommands.py
@@ -264,6 +264,7 @@ def manifest(web, req, tmpl):
     node = ctx.node()
 
     files = {}
+    dirs = {}
     parity = paritygen(web.stripecount)
 
     if path and path[-1] != "/":
@@ -275,20 +276,25 @@ def manifest(web, req, tmpl):
         if f[:l] != path:
             continue
         remain = f[l:]
-        idx = remain.find('/')
-        if idx != -1:
-            remain = remain[:idx+1]
-            n = None
-        files[remain] = (f, n)
+        elements = remain.split('/')
+        if len(elements) == 1:
+            files[remain] = f
+        else:
+            h = dirs # need to retain ref to dirs (root)
+            for elem in elements[0:-1]:
+                if elem not in h:
+                    h[elem] = {}
+                h = h[elem]
+                if len(h) > 1:
+                    break
+            h[None] = None # denotes files present
 
-    if not files:
+    if not files and not dirs:
         raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
 
     def filelist(**map):
         for f in util.sort(files):
-            full, fnode = files[f]
-            if not fnode:
-                continue
+            full = files[f]
 
             fctx = ctx.filectx(full)
             yield {"file": full,
@@ -299,14 +305,21 @@ def manifest(web, req, tmpl):
                    "permissions": mf.flags(full)}
 
     def dirlist(**map):
-        for f in util.sort(files):
-            full, fnode = files[f]
-            if fnode:
-                continue
+        for d in util.sort(dirs):
 
+            emptydirs = []
+            h = dirs[d]
+            while isinstance(h, dict) and len(h) == 1:
+                k,v = h.items()[0]
+                if v:
+                    emptydirs.append(k)
+                h = v
+
+            path = "%s%s" % (abspath, d)
             yield {"parity": parity.next(),
-                   "path": "%s%s" % (abspath, f),
-                   "basename": f[:-1]}
+                   "path": path,
+                   "emptydirs": "/".join(emptydirs),
+                   "basename": d}
 
     return tmpl("manifest",
                 rev=ctx.rev(),
diff --git a/templates/coal/map b/templates/coal/map
--- a/templates/coal/map
+++ b/templates/coal/map
@@ -23,7 +23,7 @@ searchentry = shortlogentry.tmpl
 changeset = changeset.tmpl
 manifest = manifest.tmpl
 
-direntry = '<tr class="fileline parity{parity}"><td class="name"><a href="{url}file/{node|short}{path|urlescape}{sessionvars%urlparameter}"><img src="{staticurl}coal-folder.png"> {basename|escape}/</a><td class="size"></td><td class="permissions">drwxr-xr-x</td></tr>'
+direntry = '<tr class="fileline parity{parity}"><td class="name"><a href="{url}file/{node|short}{path|urlescape}{sessionvars%urlparameter}"><img src="{staticurl}coal-folder.png"> {basename|escape}/</a> <a href="{url}file/{node|short}{path|urlescape}/{emptydirs|urlescape}{sessionvars%urlparameter}">{emptydirs|escape}</a><td class="size"></td><td class="permissions">drwxr-xr-x</td></tr>'
 fileentry = '<tr class="fileline parity{parity}"><td class="filename"><a href="{url}file/{node|short}/{file|urlescape}{sessionvars%urlparameter}#l1"><img src="{staticurl}coal-file.png"> {basename|escape}</a></td><td class="size">{size}</td><td class="permissions">{permissions|permissions}</td></tr>'
 
 filerevision = filerevision.tmpl
diff --git a/templates/gitweb/map b/templates/gitweb/map
--- a/templates/gitweb/map
+++ b/templates/gitweb/map
@@ -19,7 +19,7 @@ changelogentry = changelogentry.tmpl
 searchentry = changelogentry.tmpl
 changeset = changeset.tmpl
 manifest = manifest.tmpl
-direntry = '<tr class="parity#parity#"><td style="font-family:monospace">drwxr-xr-x</td><td style="font-family:monospace"></td><td style="font-family:monospace"></td><td><a href="#url#file/#node|short##path|urlescape#{sessionvars%urlparameter}">#basename|escape#</a></td><td class="link"><a href="#url#file/#node|short##path|urlescape#{sessionvars%urlparameter}">files</a></td></tr>'
+direntry = '<tr class="parity#parity#"><td style="font-family:monospace">drwxr-xr-x</td><td style="font-family:monospace"></td><td style="font-family:monospace"></td><td><a href="#url#file/#node|short##path|urlescape#{sessionvars%urlparameter}">#basename|escape#</a> <a href="#url#file/#node|short##path|urlescape#/#emptydirs|urlescape#{sessionvars%urlparameter}">#emptydirs|escape#</a></td><td class="link"><a href="#url#file/#node|short##path|urlescape#{sessionvars%urlparameter}">files</a></td></tr>'
 fileentry = '<tr class="parity#parity#"><td style="font-family:monospace">#permissions|permissions#</td><td style="font-family:monospace" align=right>#date|isodate#</td><td style="font-family:monospace" align=right>#size#</td><td class="list"><a class="list" href="#url#file/#node|short#/#file|urlescape#{sessionvars%urlparameter}">#basename|escape#</a></td><td class="link"><a href="#url#file/#node|short#/#file|urlescape#{sessionvars%urlparameter}">file</a> | <a href="#url#log/#node|short#/#file|urlescape#{sessionvars%urlparameter}">revisions</a> | <a href="#url#annotate/#node|short#/#file|urlescape#{sessionvars%urlparameter}">annotate</a></td></tr>'
 filerevision = filerevision.tmpl
 fileannotate = fileannotate.tmpl
diff --git a/templates/map b/templates/map
--- a/templates/map
+++ b/templates/map
@@ -19,7 +19,7 @@ changelogentry = changelogentry.tmpl
 searchentry = changelogentry.tmpl
 changeset = changeset.tmpl
 manifest = manifest.tmpl
-direntry = '<tr class="parity#parity#"><td><tt>drwxr-xr-x</tt>&nbsp;<td>&nbsp;<td>&nbsp;<td><a href="#url#file/#node|short##path|urlescape#{sessionvars%urlparameter}">#basename|escape#/</a>'
+direntry = '<tr class="parity#parity#"><td><tt>drwxr-xr-x</tt>&nbsp;<td>&nbsp;<td>&nbsp;<td><a href="#url#file/#node|short##path|urlescape#{sessionvars%urlparameter}">#basename|escape#/</a> <a href="#url#file/#node|short##path|urlescape#/#emptydirs|urlescape#{sessionvars%urlparameter}">#emptydirs|urlescape#</a>'
 fileentry = '<tr class="parity#parity#"><td><tt>#permissions|permissions#</tt>&nbsp;<td align=right><tt class="date">#date|isodate#</tt>&nbsp;<td align=right><tt>#size#</tt>&nbsp;<td><a href="#url#file/#node|short#/#file|urlescape#{sessionvars%urlparameter}">#basename|escape#</a>'
 filerevision = filerevision.tmpl
 fileannotate = fileannotate.tmpl
diff --git a/templates/paper/map b/templates/paper/map
--- a/templates/paper/map
+++ b/templates/paper/map
@@ -23,7 +23,7 @@ searchentry = ../coal/shortlogentry.tmpl
 changeset = ../coal/changeset.tmpl
 manifest = ../coal/manifest.tmpl
 
-direntry = '<tr class="fileline parity{parity}"><td class="name"><a href="{url}file/{node|short}{path|urlescape}{sessionvars%urlparameter}"><img src="{staticurl}coal-folder.png"> {basename|escape}/</a><td class="size"></td><td class="permissions">drwxr-xr-x</td></tr>'
+direntry = '<tr class="fileline parity{parity}"><td class="name"><a href="{url}file/{node|short}{path|urlescape}{sessionvars%urlparameter}"><img src="{staticurl}coal-folder.png"> {basename|escape}/</a> <a href="{url}file/{node|short}{path|urlescape}/{emptydirs|urlescape}{sessionvars%urlparameter}">{emptydirs|escape}</a><td class="size"></td><td class="permissions">drwxr-xr-x</td></tr>'
 fileentry = '<tr class="fileline parity{parity}"><td class="filename"><a href="{url}file/{node|short}/{file|urlescape}#l1{sessionvars%urlparameter}"><img src="{staticurl}coal-file.png"> {basename|escape}</a></td><td class="size">{size}</td><td class="permissions">{permissions|permissions}</td></tr>'
 
 filerevision = ../coal/filerevision.tmpl
diff --git a/tests/test-hgweb-descend-empties b/tests/test-hgweb-descend-empties
new file mode 100755
--- /dev/null
+++ b/tests/test-hgweb-descend-empties
@@ -0,0 +1,28 @@
+#!/bin/sh
+# Test chains of near empty directories, terminating 3 different ways:
+# - a1: file at level 4 (deepest)
+# - b1: two dirs at level 3
+# - e1: file at level 2
+
+echo % Set up the repo
+hg init test
+cd test
+mkdir -p a1/a2/a3/a4
+mkdir -p b1/b2/b3/b4
+mkdir -p b1/b2/c3/c4
+mkdir -p d1/d2/d3/d4
+echo foo > a1/a2/a3/a4/foo
+echo foo > b1/b2/b3/b4/foo
+echo foo > b1/b2/c3/c4/foo
+echo foo > d1/d2/d3/d4/foo
+echo foo > d1/d2/foo
+hg ci -Ama
+
+hg serve -n test -p $HGPORT -d --pid-file=hg.pid -E errors.log
+cat hg.pid >> $DAEMON_PIDS
+
+echo % manifest with descending
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/file'
+
+echo % ERRORS ENCOUNTERED
+cat errors.log
diff --git a/tests/test-hgweb-descend-empties.out b/tests/test-hgweb-descend-empties.out
new file mode 100644
--- /dev/null
+++ b/tests/test-hgweb-descend-empties.out
@@ -0,0 +1,51 @@
+% Set up the repo
+adding a1/a2/a3/a4/foo
+adding b1/b2/b3/b4/foo
+adding b1/b2/c3/c4/foo
+adding d1/d2/d3/d4/foo
+adding d1/d2/foo
+% manifest with descending
+200 Script output follows
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+<link rel="icon" href="/static/hgicon.png" type="image/png">
+<meta name="robots" content="index, nofollow" />
+<link rel="stylesheet" href="/static/style.css" type="text/css" />
+
+<title>test: files for changeset 9087c84a0f5d</title>
+</head>
+<body>
+
+<div class="buttons">
+<a href="/log/0">changelog</a>
+<a href="/shortlog/0">shortlog</a>
+<a href="/graph">graph</a>
+<a href="/tags">tags</a>
+<a href="/rev/9087c84a0f5d">changeset</a>
+
+</div>
+
+<h2>files for changeset 9087c84a0f5d: /</h2>
+
+<table cellpadding="0" cellspacing="0">
+<tr class="parity0">
+  <td><tt>drwxr-xr-x</tt>&nbsp;
+  <td>&nbsp;
+  <td>&nbsp;
+  <td><a href="/file/9087c84a0f5d/">[up]</a>
+</tr>
+<tr class="parity1"><td><tt>drwxr-xr-x</tt>&nbsp;<td>&nbsp;<td>&nbsp;<td><a href="/file/9087c84a0f5d/a1">a1/</a> <a href="/file/9087c84a0f5d/a1/a2/a3/a4">a2/a3/a4</a><tr class="parity0"><td><tt>drwxr-xr-x</tt>&nbsp;<td>&nbsp;<td>&nbsp;<td><a href="/file/9087c84a0f5d/b1">b1/</a> <a href="/file/9087c84a0f5d/b1/b2">b2</a><tr class="parity1"><td><tt>drwxr-xr-x</tt>&nbsp;<td>&nbsp;<td>&nbsp;<td><a href="/file/9087c84a0f5d/d1">d1/</a> <a href="/file/9087c84a0f5d/d1/d2">d2</a>
+
+</table>
+
+<div class="logo">
+<a href="http://www.selenic.com/mercurial/">
+<img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial"></a>
+</div>
+
+</body>
+</html>
+
+% ERRORS ENCOUNTERED