about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <e@80x24.org>2017-02-15 22:35:18 +0000
committerEric Wong <e@80x24.org>2017-02-16 04:27:50 +0000
commitf6244586ba4f5a5e7575e1254be8c9bbe303fce9 (patch)
treed1a8dd757b632f8cff66e3878f7cfc01e6e2f12d
parent993dc7a772b10deda15733c8e750d98bf4d27bd0 (diff)
downloadpublic-inbox-f6244586ba4f5a5e7575e1254be8c9bbe303fce9.tar.gz
Query strings make endpoint caching more difficult since
they're order-independent.  They are also more likely lost
or truncated inadvertantly when copy+pasting, so try to
avoid them for default endpoints.

There's still some things which are broken and followup
commits will be needed to fix them.
-rw-r--r--lib/PublicInbox/RepoBase.pm1
-rw-r--r--lib/PublicInbox/RepoGit.pm6
-rw-r--r--lib/PublicInbox/RepoGitAtom.pm15
-rw-r--r--lib/PublicInbox/RepoGitBlob.pm5
-rw-r--r--lib/PublicInbox/RepoGitCommit.pm33
-rw-r--r--lib/PublicInbox/RepoGitDiff.pm5
-rw-r--r--lib/PublicInbox/RepoGitDiffCommon.pm7
-rw-r--r--lib/PublicInbox/RepoGitLog.pm27
-rw-r--r--lib/PublicInbox/RepoGitPatch.pm7
-rw-r--r--lib/PublicInbox/RepoGitPlain.pm5
-rw-r--r--lib/PublicInbox/RepoGitQuery.pm2
-rw-r--r--lib/PublicInbox/RepoGitSnapshot.pm4
-rw-r--r--lib/PublicInbox/RepoGitSummary.pm14
-rw-r--r--lib/PublicInbox/RepoGitTag.pm11
-rw-r--r--lib/PublicInbox/RepoGitTree.pm54
-rw-r--r--lib/PublicInbox/Repobrowse.pm12
-rw-r--r--t/repobrowse_git_atom.t6
-rw-r--r--t/repobrowse_git_commit.t14
-rw-r--r--t/repobrowse_git_plain.t7
-rw-r--r--t/repobrowse_git_tree.t6
20 files changed, 102 insertions, 139 deletions
diff --git a/lib/PublicInbox/RepoBase.pm b/lib/PublicInbox/RepoBase.pm
index 97f13b25..668e9711 100644
--- a/lib/PublicInbox/RepoBase.pm
+++ b/lib/PublicInbox/RepoBase.pm
@@ -3,7 +3,6 @@
 package PublicInbox::RepoBase;
 use strict;
 use warnings;
-require PublicInbox::RepoGitQuery;
 use PublicInbox::Hval;
 our %MIME_TYPE_WHITELIST = ('application/pdf' => 1);
 
diff --git a/lib/PublicInbox/RepoGit.pm b/lib/PublicInbox/RepoGit.pm
index b44457ca..114eb656 100644
--- a/lib/PublicInbox/RepoGit.pm
+++ b/lib/PublicInbox/RepoGit.pm
@@ -50,17 +50,17 @@ sub git_dec_links ($$) {
                         $h = PublicInbox::Hval->utf8($h);
                         my $r = $h->as_href;
                         $h = $h->as_html;
-                        push @l, qq($s -&gt; <a\nhref="${rel}log?h=$r">$h</a>);
+                        push @l, qq($s -&gt; <a\nhref="${rel}log/$r">$h</a>);
                 } elsif (s/\Atag: //) {
                         my $h = PublicInbox::Hval->utf8($_);
                         my $r = $h->as_href;
                         $h = $h->as_html;
-                        push @l, qq(<a\nhref="${rel}tag?h=$r"><b>$h</b></a>);
+                        push @l, qq(<a\nhref="${rel}tag/$r"><b>$h</b></a>);
                 } else {
                         my $h = PublicInbox::Hval->utf8($_);
                         my $r = $h->as_href;
                         $h = $h->as_html;
-                        push @l, qq(<a\nhref="${rel}log?h=$r">$h</a>);
+                        push @l, qq(<a\nhref="${rel}log/$r">$h</a>);
                 }
         }
         @l;
diff --git a/lib/PublicInbox/RepoGitAtom.pm b/lib/PublicInbox/RepoGitAtom.pm
index 6d0caa02..cf4df11c 100644
--- a/lib/PublicInbox/RepoGitAtom.pm
+++ b/lib/PublicInbox/RepoGitAtom.pm
@@ -46,7 +46,7 @@ sub flush_hdr ($$$) {
         $$dst .= '</published>';
         $$dst .= qq(<link\nrel="alternate"\ntype="text/html"\nhref=");
         $$dst .= $url;
-        $$dst .= '/commit?id=';
+        $$dst .= '/commit/';
 
         my $H = $hdr->{H};
         $$dst .= $H;
@@ -67,7 +67,7 @@ sub git_atom_sed ($$) {
         my $rel = $req->{relcmd};
         my $repo_info = $req->{repo_info};
         my $title = join('/', $repo_info->{repo}, @{$req->{extra}});
-        $title = utf8_html("$title, branch $req->{q}->{h}");
+        $title = utf8_html("$title, $req->{-tip}");
         my $url = repo_root_url($self, $req);
         my $hdr = {};
         my $subtitle = $repo_info->desc_html;
@@ -76,7 +76,7 @@ sub git_atom_sed ($$) {
                 qq(<title>$title</title>) .
                 qq(<subtitle>$subtitle</subtitle>) .
                 qq(<link\nrel="alternate"\ntype="text/html"\nhref="$url"\n/>);
-        my ($plinks, $id, $ai);
+        my ($plinks, $ai);
         my $end = '';
         my $blines;
         sub {
@@ -140,12 +140,11 @@ sub call_git_atom {
 
         my $git = $repo_info->{git};
         my $env = $req->{env};
-        my $q =$req->{'q'} = PublicInbox::RepoGitQuery->new($env);
-        my $h = $q->{h};
+        my $tip = $req->{-tip};
         my $read_log = sub {
                 my $cmd = $git->cmd(qw(log --no-notes --no-color
                                         --abbrev-commit), $git->abbrev,
-                                        $ATOM_FMT, "-$max", $h, '--');
+                                        $ATOM_FMT, "-$max", $tip, '--');
                 my $expath = $req->{expath};
                 push @$cmd, $expath if $expath ne '';
                 my $rdr = { 2 => $git->err_begin };
@@ -155,13 +154,13 @@ sub call_git_atom {
 
         sub {
                 $env->{'qspawn.response'} = $_[0];
-                return $read_log->() if $h ne '';
+                return $read_log->() if $tip ne '';
 
                 my $cmd = $git->cmd(qw(symbolic-ref --short HEAD));
                 my $rdr = { 2 => $git->err_begin };
                 my $qsp = PublicInbox::Qspawn->new($cmd, undef, undef, $rdr);
                 $qsp->psgi_qx($env, undef, sub {
-                        chomp($h = ${$_[0]});
+                        chomp($tip = ${$_[0]});
                         $read_log->();
                 })
         }
diff --git a/lib/PublicInbox/RepoGitBlob.pm b/lib/PublicInbox/RepoGitBlob.pm
index 586b4acc..f9c28c22 100644
--- a/lib/PublicInbox/RepoGitBlob.pm
+++ b/lib/PublicInbox/RepoGitBlob.pm
@@ -12,10 +12,7 @@ our @EXPORT = qw(git_blob_mime_type git_blob_stream_response);
 sub call_git_blob {
         my ($self, $req) = @_;
         my $git = $req->{repo_info}->{git};
-        my $q = PublicInbox::RepoGitQuery->new($req->{env});
-        my $id = $q->{id};
-        $id eq '' and $id = 'HEAD';
-        $id .= ":$req->{expath}";
+        my $id = $req->{-tip} . ':' . $req->{expath};
 
         my ($cat, $hex, $type, $size) = $git->cat_file_begin($id);
         return unless defined $cat;
diff --git a/lib/PublicInbox/RepoGitCommit.pm b/lib/PublicInbox/RepoGitCommit.pm
index e98c3c18..c1cf06db 100644
--- a/lib/PublicInbox/RepoGitCommit.pm
+++ b/lib/PublicInbox/RepoGitCommit.pm
@@ -37,11 +37,9 @@ sub commit_header {
         my @p = split(' ', $p);
 
         my $rel = $req->{relcmd};
-        my $q = $req->{'q'};
-        my $qs = $req->{qs} = $q->qs(id => $h);
         my $x = $self->html_start($req, $s) . "\n" .
-                qq(   commit $H (<a\nhref="${rel}patch$qs">patch</a>)\n) .
-                qq(     tree <a\nrel=nofollow\nhref="${rel}tree?id=$h">$t</a>);
+                qq(   commit $H (<a\nhref="${rel}patch/$h">patch</a>)\n) .
+                qq(     tree <a\nrel=nofollow\nhref="${rel}tree/$h">$t</a>);
 
         my $git = $req->{repo_info}->{git};
         # extra show path information, if any
@@ -56,7 +54,7 @@ sub commit_header {
                         my $e = PublicInbox::Hval->utf8($_, join('/', @t));
                         $ep = $e->as_path;
                         my $eh = $e->as_html;
-                        $ep = "${rel}tree/$ep?id=$h";
+                        $ep = "${rel}tree/$ep/$h";
                         qq(<a\nrel=nofollow\nhref="$ep">$eh</a>);
                 } @$extra);
                 $path = "/$ep";
@@ -66,10 +64,10 @@ sub commit_header {
         my $np = scalar @p;
         if ($np == 1) {
                 my $p = $p[0];
-                $x .= git_parent_line('   parent', $p, $q, $git, $rel, $path);
+                $x .= git_parent_line('   parent', $p, $git, $rel);
         } elsif ($np > 1) {
                 $req->{mhelp} = CC_MERGE;
-                my @common = ($q, $git, $rel, $path);
+                my @common = ($git, $rel);
                 my @t = @p;
                 my $p = shift @t;
                 $x .= git_parent_line('  parents', $p, @common);
@@ -119,24 +117,20 @@ sub git_commit_sed ($$) {
 sub call_git_commit { # RepoBase calls this
         my ($self, $req) = @_;
         my $env = $req->{env};
-        my $q = PublicInbox::RepoGitQuery->new($env);
-        my $id = $q->{id};
-        $id eq '' and $id = 'HEAD';
+        my $tip = $req->{-tip};
 
         my $expath = $req->{expath};
         if ($expath ne '') {
                 my $relup = join('', map { '../' } @{$req->{extra}});
-                my $qs = $q->qs;
-                return $self->r(301, $req, "$relup$qs#".to_attr($expath));
+                return $self->r(301, $req, "$relup#".to_attr($expath));
         }
 
         my $git = $req->{repo_info}->{git};
         my $cmd = $git->cmd(qw(show -z --numstat -p --encoding=UTF-8
                         --no-notes --no-color -c),
-                        $git->abbrev, GIT_FMT, $id, '--');
+                        $git->abbrev, GIT_FMT, $tip, '--');
         my $rdr = { 2 => $git->err_begin };
         my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr);
-        $req->{'q'} = $q;
         $env->{'qspawn.quiet'} = 1;
         $qsp->psgi_return($env, undef, sub { # parse header
                 my ($r, $bref) = @_;
@@ -159,8 +153,7 @@ sub git_commit_404 {
 
         my $try = 'try';
         $x = "<html><head><title>$x</title></head><body><pre><b>$x</b>\n\n";
-        my $qs = $req->{'q'}->qs(id => '');
-        $x .= "<a\nhref=\"$pfx$qs\">$try the latest commit in HEAD</a>\n";
+        $x .= "<a\nhref=\"$pfx\">$try the latest commit in HEAD</a>\n";
         $x .= '</pre></body>';
 
         [404, ['Content-Type'=>'text/html'], [ $x ]];
@@ -168,11 +161,10 @@ sub git_commit_404 {
 
 # FIXME: horrifically expensive...
 sub git_parent_line {
-        my ($pfx, $p, $q, $git, $rel, $path) = @_;
-        my $qs = $q->qs(id => $p);
+        my ($pfx, $p, $git, $rel) = @_;
         my $t = git_commit_title($git, $p);
         $t = defined $t ? utf8_html($t) : '';
-        $pfx . qq( <a\nid=P\nhref="${rel}commit$path$qs">$p</a> $t\n);
+        $pfx . qq( <a\nid=P\nhref="${rel}commit/$p">$p</a> $t\n);
 }
 
 # do not break anchor links if the combined diff doesn't show changes:
@@ -186,14 +178,13 @@ sub show_unchanged {
                 qq( See the <a\nhref="#P">parents</a>, ) .
                 "or view final state(s) below:\n\n";
         my $rel = $req->{relcmd};
-        my $qs = $req->{qs};
         foreach my $anchor (@unchanged) {
                 my $fn = $anchors->{$anchor};
                 my $p = PublicInbox::Hval->utf8(git_unquote($fn));
                 $p = $p->as_path;
                 $fn = utf8_html($fn);
                 $$dst .= qq(\t<a\nrel=nofollow);
-                $$dst .= qq(\nid="$anchor"\nhref="${rel}tree/$p$qs">);
+                $$dst .= qq(\nid="$anchor"\nhref="${rel}tree/$p">);
                 $$dst .= "$fn</a>\n";
         }
 }
diff --git a/lib/PublicInbox/RepoGitDiff.pm b/lib/PublicInbox/RepoGitDiff.pm
index bb71e738..ef4717ac 100644
--- a/lib/PublicInbox/RepoGitDiff.pm
+++ b/lib/PublicInbox/RepoGitDiff.pm
@@ -34,11 +34,8 @@ sub git_diff_sed ($$) {
 
 sub call_git_diff {
         my ($self, $req) = @_;
+        my ($id, $id2) = split(/\.\./, $req->{h});
         my $env = $req->{env};
-        my $q = PublicInbox::RepoGitQuery->new($env);
-        my $id = $q->{id};
-        my $id2 = $q->{id2};
-
         my $git = $req->{repo_info}->{git};
         my $cmd = $git->cmd(qw(diff-tree -z --numstat -p --encoding=UTF-8
                                 --no-color -M -B -D -r), $id2, $id, '--');
diff --git a/lib/PublicInbox/RepoGitDiffCommon.pm b/lib/PublicInbox/RepoGitDiffCommon.pm
index 0604f9dd..3e3ea4ee 100644
--- a/lib/PublicInbox/RepoGitDiffCommon.pm
+++ b/lib/PublicInbox/RepoGitDiffCommon.pm
@@ -68,17 +68,16 @@ sub git_diff_ab_hunk ($$$$) {
                 $na = defined $na ? "#n$na" : '';
                 my $p = $req->{p}->[0];
                 $rv .= qq(<a\nrel=nofollow);
-                $rv .= qq(\nhref="${rel}tree/$req->{path_a}?id=$p$na">);
+                $rv .= qq(\nhref="${rel}tree/$p/$req->{path_a}$na">);
                 $rv .= "$ca</a>";
         }
         $rv .= ' ';
         if (defined($nb) && $nb == 0) { # deleted file
                 $rv .= $cb;
         } else {
-                my $h = $req->{h};
                 $nb = defined $nb ? "#n$nb" : '';
                 $rv .= qq(<a\nrel=nofollow);
-                $rv .= qq(\nhref="${rel}tree/$req->{path_b}?id=$h$nb">);
+                $rv .= qq(\nhref="${rel}tree/$req->{-tip}/$req->{path_b}$nb">);
                 $rv .= "$cb</a>";
         }
         $rv . ' @@' . utf8_html($ctx);
@@ -129,7 +128,7 @@ sub git_diff_cc_hunk {
         } else {
                 my $h = $req->{h};
                 $rv .= qq( <a\nrel=nofollow);
-                $rv .= qq(\nhref="${rel}tree/$path?id=$h#n$n">$last</a>);
+                $rv .= qq(\nhref="${rel}tree/$h/$path#n$n">$last</a>);
         }
         $rv .= " $at" . utf8_html($ctx);
 }
diff --git a/lib/PublicInbox/RepoGitLog.pm b/lib/PublicInbox/RepoGitLog.pm
index 9cfa526e..09409edd 100644
--- a/lib/PublicInbox/RepoGitLog.pm
+++ b/lib/PublicInbox/RepoGitLog.pm
@@ -27,18 +27,18 @@ sub parent_links {
 
 sub flush_log_hdr ($$$) {
         my ($req, $dst, $hdr) = @_;
-        my $rel = $req->{relcmd};
+        my $lpfx = $req->{lpfx};
         my $seen = $req->{seen};
         $$dst .= '<hr /><pre>' if scalar keys %$seen;
         my $id = $hdr->{h};
         $seen->{$id} = 1;
         $$dst .= qq(<a\nid=p$id\n);
-        $$dst .= qq(href="${rel}commit?id=$id"><b>);
+        $$dst .= qq(href="${lpfx}commit/$id"><b>);
         $$dst .= utf8_html($hdr->{'s'}); # FIXME may still OOM
         $$dst .= '</b></a>';
         my $D = $hdr->{D}; # FIXME: thousands of decorations may OOM us
         if ($D ne '') {
-                $$dst .= ' (' . join(', ', git_dec_links($rel, $D)) . ')';
+                $$dst .= ' (' . join(', ', git_dec_links($lpfx, $D)) . ')';
         }
         my @p = split(/ /, $hdr->{p});
         push @{$req->{parents}}, @p;
@@ -56,14 +56,14 @@ sub git_log_sed_end ($$) {
         my $np = 0;
         my $seen = $req->{seen};
         my $git = $req->{repo_info}->{git};
-        my $rel = $req->{relcmd};
+        my $lpfx = $req->{lpfx};
         foreach my $p (@{$req->{parents}}) {
                 next if $seen->{$p};
                 $seen->{$p} = ++$np;
                 my $s = git_commit_title($git, $p);
-                $m .= qq(\n<a\nid=p$p\nhref="?h=$p">$p</a>\t);
+                $m .= qq(\n<a\nid=p$p\nhref="$p">$p</a>\t);
                 $s = defined($s) ? utf8_html($s) : '';
-                $m .= qq(<a\nhref="${rel}commit?id=$p">$s</a>);
+                $m .= qq(<a\nhref="${lpfx}commit/$p">$s</a>);
         }
         if ($np == 0) {
                 $$dst .= "No commits follow";
@@ -123,17 +123,22 @@ sub call_git_log {
         my ($self, $req) = @_;
         my $repo_info = $req->{repo_info};
         my $max = $repo_info->{max_commit_count} || 50;
+        my $h = $req->{h};
         $max = int($max);
         $max = 50 if $max == 0;
         my $env = $req->{env};
-        my $q = $req->{'q'} = PublicInbox::RepoGitQuery->new($env);
-        my $h = $q->{h};
-        $h eq '' and $h = 'HEAD';
         my $git = $repo_info->{git};
         my $cmd = $git->cmd(qw(log --no-notes --no-color --abbrev-commit),
-                                $git->abbrev, $LOG_FMT, "-$max", $h, '--');
+                                $git->abbrev, $LOG_FMT, "-$max",
+                                $req->{-tip}, '--');
         my $rdr = { 2 => $git->err_begin };
-        my $title = "log: $repo_info->{repo} (" . utf8_html($h). ')';
+        my $title = "log: $repo_info->{repo}";
+        if (defined $h) {
+                $title .= ' ('. utf8_html($h). ')';
+                $req->{lpfx} = $req->{relcmd};
+        } else {
+                $req->{lpfx} = $req->{relcmd}.$req->{-tip};
+        }
         $req->{lhtml} = $self->html_start($req, $title) . "\n\n";
         my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr);
         $qsp->psgi_return($env, undef, sub {
diff --git a/lib/PublicInbox/RepoGitPatch.pm b/lib/PublicInbox/RepoGitPatch.pm
index e9227b6f..d851457c 100644
--- a/lib/PublicInbox/RepoGitPatch.pm
+++ b/lib/PublicInbox/RepoGitPatch.pm
@@ -17,13 +17,12 @@ sub call_git_patch {
         my ($self, $req) = @_;
         my $git = $req->{repo_info}->{git};
         my $env = $req->{env};
-        my $q = PublicInbox::RepoGitQuery->new($env);
-        my $id = $q->{id};
-        $id =~ /\A[\w-]+([~\^][~\^\d])*\z/ or $id = 'HEAD';
+        my $tip = $req->{-tip};
+        $tip =~ /\A[\w-]+([~\^][~\^\d])*\z/;
 
         # limit scope, don't take extra args to avoid wasting server
         # resources buffering:
-        my $range = "$id~1..$id^0";
+        my $range = "$tip~1..$tip^0";
         my $cmd = $git->cmd(@CMD, $sig." $range", $range, '--');
         my $expath = $req->{expath};
         push @$cmd, $expath if $expath ne '';
diff --git a/lib/PublicInbox/RepoGitPlain.pm b/lib/PublicInbox/RepoGitPlain.pm
index 2ba24e08..6114a858 100644
--- a/lib/PublicInbox/RepoGitPlain.pm
+++ b/lib/PublicInbox/RepoGitPlain.pm
@@ -11,10 +11,7 @@ use PublicInbox::Qspawn;
 sub call_git_plain {
         my ($self, $req) = @_;
         my $git = $req->{repo_info}->{git};
-        my $q = PublicInbox::RepoGitQuery->new($req->{env});
-        my $id = $q->{id};
-        $id eq '' and $id = 'HEAD';
-        $id .= ":$req->{expath}";
+        my $id = $req->{-tip} . ':' . $req->{expath};
         my ($cat, $hex, $type, $size) = $git->cat_file_begin($id);
         return unless defined $cat;
 
diff --git a/lib/PublicInbox/RepoGitQuery.pm b/lib/PublicInbox/RepoGitQuery.pm
index 638a1316..c8d4a256 100644
--- a/lib/PublicInbox/RepoGitQuery.pm
+++ b/lib/PublicInbox/RepoGitQuery.pm
@@ -7,7 +7,7 @@ use strict;
 use warnings;
 use PublicInbox::Hval;
 use URI::Escape qw(uri_unescape);
-my @KNOWN_PARAMS = qw(id id2 h ofs);
+my @KNOWN_PARAMS = qw(id id2 ofs);
 
 sub new {
         my ($class, $env) = @_;
diff --git a/lib/PublicInbox/RepoGitSnapshot.pm b/lib/PublicInbox/RepoGitSnapshot.pm
index e05ad80c..9ba4c04a 100644
--- a/lib/PublicInbox/RepoGitSnapshot.pm
+++ b/lib/PublicInbox/RepoGitSnapshot.pm
@@ -36,9 +36,7 @@ our %FMT_TYPES = (
 sub call_git_snapshot ($$) { # invoked by PublicInbox::RepoBase::call
         my ($self, $req) = @_;
 
-        my @extra = @{$req->{extra}};
-        my $ref = shift @extra;
-        return $self->r(404) if (!defined $ref) || scalar(@extra);
+        my $ref = $req->{-tip};
         my $orig_fn = $ref;
 
         # just in case git changes refname rules, don't allow wonky filenames
diff --git a/lib/PublicInbox/RepoGitSummary.pm b/lib/PublicInbox/RepoGitSummary.pm
index e9e1458b..0ecef981 100644
--- a/lib/PublicInbox/RepoGitSummary.pm
+++ b/lib/PublicInbox/RepoGitSummary.pm
@@ -58,10 +58,10 @@ sub for_each_ref {
                 my $sref;
                 if ($type eq 'tag') {
                         $h = "<b>$h</b>";
-                        $sref = $ref = $rel . 'tag?h=' . $ref;
+                        $sref = $ref = $rel . 'tag/' . $ref;
                 } elsif ($type eq 'commit') {
-                        $sref = $rel . 'commit?h=' . $ref;
-                        $ref = $rel . 'log?h=' . $ref;
+                        $sref = $rel . 'commit/' . $ref;
+                        $ref = $rel . 'log/' . $ref;
                 } else {
                         # no point in wasting code to support tagged
                         # trees/blobs...
@@ -82,7 +82,7 @@ sub for_each_ref {
         foreach my $r (@$readme) {
                 my $doc = $git->cat_file('HEAD:'.$r);
                 defined $doc or next;
-                $fh->write('<pre>' . readme_path_links($rel, $r) .
+                $fh->write('<pre>' . readme_path_links($req, $rel, $r) .
                         " (HEAD)\n\n" . utf8_html($$doc) . '</pre>');
         }
         $fh->write('</body></html>');
@@ -90,17 +90,17 @@ sub for_each_ref {
 }
 
 sub readme_path_links {
-        my ($rel, $readme) = @_;
+        my ($req, $rel, $readme) = @_;
         my @path = split(m!/+!, $readme);
 
-        my $s = "tree <a\nhref=\"${rel}tree\">root</a>/";
+        my $s = "tree <a\nhref=\"${rel}tree/$req->{-tip}\">root</a>/";
         my @t;
         $s .= join('/', (map {
                 push @t, $_;
                 my $e = PublicInbox::Hval->utf8($_, join('/', @t));
                 my $ep = $e->as_path;
                 my $eh = $e->as_html;
-                $e = "<a\nhref=\"${rel}tree/$ep\">$eh</a>";
+                $e = "<a\nhref=\"${rel}tree/$req->{-tip}/$ep\">$eh</a>";
                 # bold the last one
                 scalar(@t) == scalar(@path) ? "<b>$e</b>" : $e;
         } @path));
diff --git a/lib/PublicInbox/RepoGitTag.pm b/lib/PublicInbox/RepoGitTag.pm
index 96835b2c..d046f853 100644
--- a/lib/PublicInbox/RepoGitTag.pm
+++ b/lib/PublicInbox/RepoGitTag.pm
@@ -19,9 +19,8 @@ my %cmd_map = ( # type => action
 sub call_git_tag {
         my ($self, $req) = @_;
 
-        my $q = PublicInbox::RepoGitQuery->new($req->{env});
-        my $h = $q->{h};
-        $h eq '' and return git_tag_list($self, $req);
+        my $h = $req->{h};
+        defined $h or return git_tag_list($self, $req);
         sub {
                 my ($res) = @_;
                 git_tag_show($self, $req, $h, $res);
@@ -58,7 +57,7 @@ sub git_show_tag_as_tag {
         my $label = "$type $obj";
         my $cmd = $cmd_map{$type} || 'show';
         my $rel = $req->{relcmd};
-        my $obj_link = qq(<a\nhref="$rel$cmd?id=$obj">$label</a>);
+        my $obj_link = qq(<a\nhref="$rel$cmd/$obj">$label</a>);
         $head = $h . "\n\n   tag <b>$tag</b>\nobject $obj_link\n";
         if (my $tagger = $h{tagger}) {
                 $head .= 'tagger ' . join("\t", creator_split($tagger)) . "\n";
@@ -147,7 +146,7 @@ sub git_each_tag_sed ($$) {
                         my $h = $ref->as_html;
                         $ref = $ref->as_href;
                         $dst .= qq(<tr><td><tt>) .
-                                qq(<a\nhref="?h=$ref"><b>$h</b></a>) .
+                                qq(<a\nhref="tag/$ref"><b>$h</b></a>) .
                                 qq(</tt></td><td><tt>$date</tt></td><td><tt>) .
                                 utf8_html($s) . '</tt></td></tr>';
                 }
@@ -189,7 +188,7 @@ sub unknown_tag_type {
         my $rel = $req->{relcmd};
         my $label = "$type $hex";
         my $cmd = $cmd_map{$type} || 'show';
-        my $obj_link = qq(<a\nhref="$rel$cmd?id=$hex">$label</a>\n);
+        my $obj_link = qq(<a\nhref="$rel$cmd/$hex">$label</a>\n);
 
         $fh->write($self->html_start($req,
                                 "$repo_info->{repo}: ref: $h") .
diff --git a/lib/PublicInbox/RepoGitTree.pm b/lib/PublicInbox/RepoGitTree.pm
index 4a68cf69..840af9ad 100644
--- a/lib/PublicInbox/RepoGitTree.pm
+++ b/lib/PublicInbox/RepoGitTree.pm
@@ -21,14 +21,7 @@ sub call_git_tree {
         my ($self, $req) = @_;
         my @extra = @{$req->{extra}};
         my $git = $req->{repo_info}->{git};
-        my $q = PublicInbox::RepoGitQuery->new($req->{env});
-        my $id = $q->{id};
-        if ($id eq '') {
-                chomp($id = $git->qx(qw(rev-parse --short=10 HEAD)));
-                $q->{id} = $id;
-        }
-
-        my $obj = "$id:$req->{expath}";
+        my $obj = "$req->{-tip}:$req->{expath}";
         my ($hex, $type, $size) = $git->check($obj);
 
         unless (defined($type)) {
@@ -41,7 +34,7 @@ sub call_git_tree {
         if ($type eq 'tree') {
                 $opts->{noindex} = 1;
                 $req->{thtml} = $self->html_start($req, $title, $opts) . "\n";
-                git_tree_show($req, $hex, $q);
+                git_tree_show($req, $hex);
         } elsif ($type eq 'blob') {
                 sub {
                         my $res = $_[0];
@@ -49,7 +42,7 @@ sub call_git_tree {
                                 ['Content-Type','text/html; charset=UTF-8']]);
                         $fh->write($self->html_start($req, $title, $opts) .
                                         "\n");
-                        git_blob_show($req, $fh, $git, $hex, $q);
+                        git_blob_show($req, $fh, $git, $hex);
                         $fh->write('</body></html>');
                         $fh->close;
                 }
@@ -60,15 +53,15 @@ sub call_git_tree {
 }
 
 sub cur_path {
-        my ($req, $q) = @_;
-        my $qs = $q->qs;
+        my ($req) = @_;
         my @ex = @{$req->{extra}} or return '<b>root</b>';
         my $s;
 
+        my $tip = $req->{-tip};
         my $rel = $req->{relcmd};
         # avoid relative paths, here, we don't want to propagate
         # trailing-slash URLs although we tolerate them
-        $s = "<a\nhref=\"${rel}tree$qs\">root</a>/";
+        $s = "<a\nhref=\"${rel}tree/$tip\">root</a>/";
         my $cur = pop @ex;
         my @t;
         $s .= join('/', (map {
@@ -76,37 +69,38 @@ sub cur_path {
                 my $e = PublicInbox::Hval->utf8($_, join('/', @t));
                 my $ep = $e->as_path;
                 my $eh = $e->as_html;
-                "<a\nhref=\"${rel}tree/$ep$qs\">$eh</a>";
+                "<a\nhref=\"${rel}tree/$tip/$ep\">$eh</a>";
         } @ex), '<b>'.utf8_html($cur).'</b>');
 }
 
 sub git_blob_show {
-        my ($req, $fh, $git, $hex, $q) = @_;
+        my ($req, $fh, $git, $hex) = @_;
         # ref: buffer_is_binary in git.git
         my $to_read = 8000; # git uses this size to detect binary files
         my $text_p;
         my $n = 0;
 
+        my $tip = $req->{-tip};
         my $rel = $req->{relcmd};
-        my $plain = join('/', "${rel}plain", @{$req->{extra}});
-        $plain = PublicInbox::Hval->utf8($plain)->as_path . $q->qs;
-        my $t = cur_path($req, $q);
-        my $h = qq{\npath: $t\n\nblob $hex};
+        my $plain = join('/', "${rel}plain/$tip", @{$req->{extra}});
+        $plain = PublicInbox::Hval->utf8($plain)->as_path;
+        my $t = cur_path($req);
+        my $s = qq{\npath: $t\n\nblob $hex};
         my $end = '';
 
         $git->cat_file($hex, sub {
                 my ($cat, $left) = @_; # $$left == $size
-                $h .= qq{\t$$left bytes (<a\nhref="$plain">raw</a>)};
+                $s .= qq{\t$$left bytes (<a\nhref="$plain">raw</a>)};
                 $to_read = $$left if $to_read > $$left;
                 my $r = read($cat, my $buf, $to_read);
                 return unless defined($r) && $r > 0;
                 $$left -= $r;
 
                 if (index($buf, "\0") >= 0) {
-                        $fh->write("$h\n$BINARY_MSG</pre>");
+                        $fh->write("$s\n$BINARY_MSG</pre>");
                         return;
                 }
-                $fh->write($h."</pre><hr/><table\nsummary=blob><tr><td><pre>");
+                $fh->write($s."</pre><hr/><table\nsummary=blob><tr><td><pre>");
                 $text_p = 1;
 
                 while (1) {
@@ -147,7 +141,6 @@ sub git_tree_sed ($) {
         my ($req) = @_;
         my @lines;
         my $buf = '';
-        my $qs = $req->{qs};
         my $pfx = $req->{tpfx};
         my $end;
         sub {
@@ -180,29 +173,30 @@ sub git_tree_sed ($) {
                         # 'plain' and 'log' links intentionally omitted
                         # for brevity and speed
                         $dst .= qq($m\t).
-                                qq($s\t<a\nhref="$pfx$ref$qs">$path</a>\n);
+                                qq($s\t<a\nhref="$pfx/$ref">$path</a>\n);
                 }
                 $dst;
         }
 }
 
 sub git_tree_show {
-        my ($req, $hex, $q) = @_;
+        my ($req, $hex) = @_;
         my $git = $req->{repo_info}->{git};
         my $cmd = $git->cmd(qw(ls-tree -l -z), $git->abbrev, $hex);
         my $rdr = { 2 => $git->err_begin };
         my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr);
-        my $t = cur_path($req, $q);
+        my $t = cur_path($req);
         my $pfx;
 
         $req->{thtml} .= "\npath: $t\n\n<b>mode\tsize\tname</b>\n";
-        $req->{qs} = $q->qs;
         if ($req->{tslash}) {
-                $pfx = './';
+                $pfx = '../';
         } elsif (defined(my $last = $req->{extra}->[-1])) {
-                $pfx = PublicInbox::Hval->utf8($last)->as_path . '/';
+                $pfx = PublicInbox::Hval->utf8($last)->as_path;
+        } elsif (defined $req->{h}) {
+                $pfx = $req->{-tip};
         } else {
-                $pfx = 'tree/';
+                $pfx = 'tree/' . $req->{-tip};
         }
         $req->{tpfx} = $pfx;
         my $env = $req->{env};
diff --git a/lib/PublicInbox/Repobrowse.pm b/lib/PublicInbox/Repobrowse.pm
index c16f10fd..2513a105 100644
--- a/lib/PublicInbox/Repobrowse.pm
+++ b/lib/PublicInbox/Repobrowse.pm
@@ -89,7 +89,7 @@ sub call {
         my $method = $env->{REQUEST_METHOD};
         return r(405, 'Method Not Allowed') if ($method !~ /\AGET|HEAD|POST\z/);
 
-        # URL syntax: / repo [ / cmd [ / path ] ]
+        # URL syntax: / repo [ / cmd [ / head [ / path ] ] ]
         # cmd: log | commit | diff | tree | view | blob | snapshot
         # repo and path (@extra) may both contain '/'
         my $path_info = $env->{PATH_INFO};
@@ -116,13 +116,16 @@ sub call {
         my $vcs_lc = $repo_info->{vcs};
         my $vcs = $VCS{$vcs_lc} or return r404();
         my $mod;
+        my $h;
         if (defined $cmd && length $cmd) {
                 $mod = $CMD{$cmd};
-                unless ($mod) {
+                if ($mod) {
+                        $h = shift @extra if @extra;
+                } else {
                         unshift @extra, $cmd;
                         $mod = 'Fallback';
                 }
-                $req->{relcmd} = '../' x scalar(@extra);
+                $req->{relcmd} = '../' x (scalar(@extra) + 1);
         } else {
                 $mod = 'Summary';
                 $cmd = 'summary';
@@ -137,7 +140,8 @@ sub call {
                 pop @extra;
                 ++$tslash;
         }
-
+        $req->{h} = $h;
+        $req->{-tip} = defined $h ? $h : 'HEAD';
         return no_tslash($env) if ($tslash && $NO_TSLASH{$mod});
 
         $req->{tslash} = $tslash;
diff --git a/t/repobrowse_git_atom.t b/t/repobrowse_git_atom.t
index 1f8e95e3..6769bf9f 100644
--- a/t/repobrowse_git_atom.t
+++ b/t/repobrowse_git_atom.t
@@ -23,11 +23,7 @@ test_psgi($test->{app}, sub {
         like($body, qr!<pre\s*[^>]+>\* header:\n  add header</pre>!,
                 'body wrapped in <pre>');
 
-        $res = $cb->(GET($req . '/'));
-        my $sl = dechunk($res);
-        is($body, $sl, 'slash returned identical to non-trailing slash');
-
-        $res = $cb->(GET($req . '/foo.txt'));
+        $res = $cb->(GET($req . '/master/foo.txt'));
         is($res->code, 200, 'got 200');
         $body = dechunk($res);
         like($body, qr{\bhref="http://[^/]+/test\.git/}, 'hrefs OK');
diff --git a/t/repobrowse_git_commit.t b/t/repobrowse_git_commit.t
index ed2d6d56..f5913023 100644
--- a/t/repobrowse_git_commit.t
+++ b/t/repobrowse_git_commit.t
@@ -8,24 +8,12 @@ test_psgi($test->{app}, sub {
         my ($cb) = @_;
         my $path = '/path/to/something';
         my $req = 'http://example.com/test.git/commit';
-        my $res = $cb->(GET($req . $path));
-        is($res->code, 301, 'got 301 to anchor');
-        is($res->header('Location'), "$req#path:to:something",
-                'redirected to anchor from path');
-
-        my $q = '?id=deadbeef';
-        $res = $cb->(GET($req . $path . $q));
-        is($res->code, 301, 'got 301 with query string');
-        is($res->header('Location'), "$req$q#path:to:something",
-                'redirected to anchor from path with query');
+        my $res;
 
         $res = $cb->(GET($req));
         is($res->code, 200, 'got proper 200 response for default');
         my $body = dechunk($res);
         like($body, qr!</html>\z!, 'response body finished');
-
-        $res = $cb->(GET($req.$q));
-        is($res->code, 404, 'got 404 response for bad id');
 });
 
 done_testing();
diff --git a/t/repobrowse_git_plain.t b/t/repobrowse_git_plain.t
index 27347f70..a93fa10e 100644
--- a/t/repobrowse_git_plain.t
+++ b/t/repobrowse_git_plain.t
@@ -7,11 +7,12 @@ my $test = require './t/repobrowse_common_git.perl';
 test_psgi($test->{app}, sub {
         my ($cb) = @_;
 
-        my $req = 'http://example.com/test.git/plain/dir';
+        my $req = 'http://example.com/test.git/plain/master/dir';
         my $res = $cb->(GET($req));
         is(200, $res->code, 'got 200 response from dir');
         my $noslash_body = dechunk($res);
-        like($noslash_body, qr{href="dir/dur">dur</a></li>}, 'path ok w/o slash');
+        like($noslash_body, qr{href="dir/dur">dur</a></li>},
+                'path ok w/o slash');
 
         my $slash = $req . '/';
         my $r2 = $cb->(GET($slash));
@@ -19,7 +20,7 @@ test_psgi($test->{app}, sub {
         my $slash_body = dechunk($r2);
         like($slash_body, qr{href="\./dur\">dur</a></li>}, 'path ok w/ slash');
 
-        $req = 'http://example.com/test.git/plain/foo.txt';
+        $req = 'http://example.com/test.git/plain/master/foo.txt';
         my $blob = $cb->(GET($req));
         like($blob->header('Content-Type'), qr!\Atext/plain\b!,
                 'got text/plain blob');
diff --git a/t/repobrowse_git_tree.t b/t/repobrowse_git_tree.t
index 531f914c..d91cfdc9 100644
--- a/t/repobrowse_git_tree.t
+++ b/t/repobrowse_git_tree.t
@@ -7,11 +7,11 @@ my $test = require './t/repobrowse_common_git.perl';
 test_psgi($test->{app}, sub {
         my ($cb) = @_;
 
-        my $req = 'http://example.com/test.git/tree/dir';
+        my $req = 'http://example.com/test.git/tree/HEAD/dir';
         my $res = $cb->(GET($req));
         is(200, $res->code, 'got 200 response from dir');
         my $noslash_body = dechunk($res);
-        like($noslash_body, qr{href="dir/dur\?id=\w+">dur/</a>},
+        like($noslash_body, qr{href="dir/dur">dur/</a>},
                 'path ok w/o slash');
 
         my $slash = $req . '/';
@@ -19,7 +19,7 @@ test_psgi($test->{app}, sub {
         is(301, $r2->code, 'got 301 response from dir with slash');
         is($req, $r2->header('Location'), 'redirected w/o slash');
 
-        $req = 'http://example.com/test.git/tree/foo.txt';
+        $req = 'http://example.com/test.git/tree/master/foo.txt';
         my $blob = $cb->(GET($req));
         is($blob->header('Content-Type'), 'text/html; charset=UTF-8',
                 'got text/html blob');