Patchwork [3,of,3,RFC] hgweb: handle obsolete changesets gracefully

login
register
mail settings
Submitter Dan Villiom Podlaski Christiansen
Date Aug. 8, 2013, 7:48 a.m.
Message ID <5507e2af80f5e8026a05.1375948126@dookie.local>
Download mbox | patch
Permalink /patch/2094/
State Changes Requested
Headers show

Comments

Dan Villiom Podlaski Christiansen - Aug. 8, 2013, 7:48 a.m.
# HG changeset patch
# User Dan Villiom Podlaski Christiansen  <danchr@gmail.com>
# Date 1375624663 -7200
#      Sun Aug 04 15:57:43 2013 +0200
# Node ID 5507e2af80f5e8026a054ce440c0c8958134eefe
# Parent  b1f7ae28c3e371a70ee363a4fbe5d50063a224fc
hgweb: handle obsolete changesets gracefully

If the changeset has any successors, issue a 403 Moved Permanently;
otherwise we issue a 410 Gone. Please note that this is slightly
misleading for 'secret' changesets, as they may appear later on.
Augie Fackler - Aug. 9, 2013, 3:12 p.m.
On Thu, Aug 08, 2013 at 09:48:46AM +0200, Dan Villiom Podlaski Christiansen wrote:
> # HG changeset patch
> # User Dan Villiom Podlaski Christiansen  <danchr@gmail.com>
> # Date 1375624663 -7200
> #      Sun Aug 04 15:57:43 2013 +0200
> # Node ID 5507e2af80f5e8026a054ce440c0c8958134eefe
> # Parent  b1f7ae28c3e371a70ee363a4fbe5d50063a224fc
> hgweb: handle obsolete changesets gracefully
>
> If the changeset has any successors, issue a 403 Moved Permanently;

I think you mean 301?

> otherwise we issue a 410 Gone. Please note that this is slightly
> misleading for 'secret' changesets, as they may appear later on.

I think you should handle these separately if possible, since secret
changesets probably want to be 404. Also, you should verify that the
destination of the 301 isn't a 404 before redirecting there, because
that /would/ potentially leak some secret changeset hash IDs.

>
> diff --git a/mercurial/hgweb/common.py b/mercurial/hgweb/common.py
> --- a/mercurial/hgweb/common.py
> +++ b/mercurial/hgweb/common.py
> @@ -9,12 +9,14 @@
>  import errno, mimetypes, os
>
>  HTTP_OK = 200
> +HTTP_MOVED_PERMANENTLY = 301
>  HTTP_NOT_MODIFIED = 304
>  HTTP_BAD_REQUEST = 400
>  HTTP_UNAUTHORIZED = 401
>  HTTP_FORBIDDEN = 403
>  HTTP_NOT_FOUND = 404
>  HTTP_METHOD_NOT_ALLOWED = 405
> +HTTP_GONE = 410
>  HTTP_SERVER_ERROR = 500
>
>
> diff --git a/mercurial/hgweb/hgweb_mod.py b/mercurial/hgweb/hgweb_mod.py
> --- a/mercurial/hgweb/hgweb_mod.py
> +++ b/mercurial/hgweb/hgweb_mod.py
> @@ -8,11 +8,13 @@
>
>  import os
>  from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
> +from mercurial import obsolete
> +from mercurial.node import short
>  from mercurial.templatefilters import websub
>  from mercurial.i18n import _
>  from common import get_stat, ErrorResponse, permhooks, caching
> -from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
> -from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
> +from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST, HTTP_GONE
> +from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_MOVED_PERMANENTLY
>  from request import wsgirequest
>  import webcommands, protocol, webutil, re
>
> @@ -249,6 +251,33 @@ class hgweb(object):
>
>              return content
>
> +        except error.FilteredLookupError, err:
> +            succsets = obsolete.successorssets(self.repo, err.rev)
> +
> +            if not succsets:
> +                req.respond(HTTP_GONE, ctype)
> +
> +                return tmpl('error', error=err.message)
> +
> +            elif len(succsets) != 1:
> +                # TODO: changeset has divergent successors
> +                req.respond(HTTP_SERVER_ERROR, ctype)
> +
> +                return tmpl('error', error=err.message)
> +
> +            location = [req.url.rstrip('/')]
> +            location += req.form['cmd']
> +
> +            location.append(short(succsets[0][-1]))
> +
> +            if 'file' in req.form:
> +                location += req.form['file']
> +
> +            req.headers.extend([('Location', '/'.join(location))])
> +            req.respond(HTTP_MOVED_PERMANENTLY, ctype)
> +
> +            return tmpl('error', error=err.message)
> +
>          except (error.LookupError, error.RepoLookupError), err:
>              req.respond(HTTP_NOT_FOUND, ctype)
>              msg = str(err)
> diff --git a/mercurial/hgweb/webutil.py b/mercurial/hgweb/webutil.py
> --- a/mercurial/hgweb/webutil.py
> +++ b/mercurial/hgweb/webutil.py
> @@ -201,6 +201,9 @@ def cleanpath(repo, path):
>  def changeidctx (repo, changeid):
>      try:
>          ctx = repo[changeid]
> +    except error.FilteredLookupError:
> +        raise
> +
>      except error.RepoError:
>          man = repo.manifest
>          ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
> diff --git a/tests/test-hgweb-commands.t b/tests/test-hgweb-commands.t
> --- a/tests/test-hgweb-commands.t
> +++ b/tests/test-hgweb-commands.t
> @@ -1385,7 +1385,7 @@ proper status for filtered revision
>    $ PATH_INFO=/rev/5; export PATH_INFO
>    $ QUERY_STRING='style=raw'
>    $ python hgweb.cgi #> search
> -  Status: 404 Not Found\r (esc)
> +  Status: 410 Gone\r (esc)
>    ETag: *\r (glob) (esc)
>    Content-Type: text/plain; charset=ascii\r (esc)
>    \r (esc)
> @@ -1399,7 +1399,7 @@ proper status for filtered revision
>    $ PATH_INFO=/rev/4; export PATH_INFO
>    $ QUERY_STRING='style=raw'
>    $ python hgweb.cgi #> search
> -  Status: 404 Not Found\r (esc)
> +  Status: 410 Gone\r (esc)
>    ETag: *\r (glob) (esc)
>    Content-Type: text/plain; charset=ascii\r (esc)
>    \r (esc)
> @@ -1498,6 +1498,47 @@ filtered '0' changeset
>
>
>
> +Test obsolete redirection
> +
> +  $ cat > ../obs.py << EOF
> +  > import mercurial.obsolete
> +  > mercurial.obsolete._enabled = True
> +  > EOF
> +  $ echo '[extensions]' >> $HGRCPATH
> +  $ echo "rebase=" >> $HGRCPATH
> +  $ echo "obs=${TESTTMP}/obs.py" >> $HGRCPATH
> +  $ hg up 12
> +  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
> +  $ echo B > b; hg add b
> +  $ hg ci -m 6
> +
> +Use a rebase to obsolete r13, and test redirection
> +
> +  $ hg rebase -r 13 -d 11
> +  $ PATH_INFO=/rev/13; export PATH_INFO
> +  $ QUERY_STRING='style=raw'
> +  $ python hgweb.cgi #> search
> +  Status: 301 Moved Permanently\r (esc)
> +  ETag: *\r (glob) (esc)
> +  Location: /test/rev/0d601cf5c587\r (esc)
> +  Content-Type: text/plain; charset=ascii\r (esc)
> +  \r (esc)
> +
> +  error: revision 13 is hidden
> +
> +Rebase r13 once more, testing divergent changesets
> +
> +  $ hg --hidden rebase -r 13 -d 9
> +  $ PATH_INFO=/rev/13; export PATH_INFO
> +  $ QUERY_STRING='style=raw'
> +  $ python hgweb.cgi #> search
> +  Status: 500 Internal Server Error\r (esc)
> +  ETag: *\r (glob) (esc)
> +  Content-Type: text/plain; charset=ascii\r (esc)
> +  \r (esc)
> +
> +  error: revision 13 is hidden
> +
>
>
>    $ cd ..
> diff --git a/tests/test-obsolete.t b/tests/test-obsolete.t
> --- a/tests/test-obsolete.t
> +++ b/tests/test-obsolete.t
> @@ -747,7 +747,7 @@ check filelog view
>    $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'rev/68'
>    200 Script output follows
>    $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'rev/67'
> -  404 Not Found
> +  410 Gone
>    [1]
>
>  check that web.view config option:
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@selenic.com
> http://selenic.com/mailman/listinfo/mercurial-devel
Pierre-Yves David - Aug. 15, 2013, 11:57 p.m.
On 8 août 2013, at 09:48, Dan Villiom Podlaski Christiansen wrote:

> # HG changeset patch
> # User Dan Villiom Podlaski Christiansen  <danchr@gmail.com>
> # Date 1375624663 -7200
> #      Sun Aug 04 15:57:43 2013 +0200
> # Node ID 5507e2af80f5e8026a054ce440c0c8958134eefe
> # Parent  b1f7ae28c3e371a70ee363a4fbe5d50063a224fc
> hgweb: handle obsolete changesets gracefully
> 
> If the changeset has any successors, issue a 403 Moved Permanently;
> otherwise we issue a 410 Gone. Please note that this is slightly
> misleading for 'secret' changesets, as they may appear later on.

You are going too fast here.

1) issue 410 for filtered changeset
1.1) (403 or 404 for secret ?)
2) issue a smarter message with potential link to successors (could include information about who and when it was rewritten)
3) use redirect (does not like the idea yet)

> diff --git a/mercurial/hgweb/common.py b/mercurial/hgweb/common.py
> --- a/mercurial/hgweb/common.py
> +++ b/mercurial/hgweb/common.py
> @@ -9,12 +9,14 @@
> import errno, mimetypes, os
> 
> HTTP_OK = 200
> +HTTP_MOVED_PERMANENTLY = 301
> HTTP_NOT_MODIFIED = 304
> HTTP_BAD_REQUEST = 400
> HTTP_UNAUTHORIZED = 401
> HTTP_FORBIDDEN = 403
> HTTP_NOT_FOUND = 404
> HTTP_METHOD_NOT_ALLOWED = 405
> +HTTP_GONE = 410
> HTTP_SERVER_ERROR = 500
> 
> 
> diff --git a/mercurial/hgweb/hgweb_mod.py b/mercurial/hgweb/hgweb_mod.py
> --- a/mercurial/hgweb/hgweb_mod.py
> +++ b/mercurial/hgweb/hgweb_mod.py
> @@ -8,11 +8,13 @@
> 
> import os
> from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
> +from mercurial import obsolete
> +from mercurial.node import short
> from mercurial.templatefilters import websub
> from mercurial.i18n import _
> from common import get_stat, ErrorResponse, permhooks, caching
> -from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
> -from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
> +from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST, HTTP_GONE
> +from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_MOVED_PERMANENTLY
> from request import wsgirequest
> import webcommands, protocol, webutil, re
> 
> @@ -249,6 +251,33 @@ class hgweb(object):
> 
>             return content
> 
> +        except error.FilteredLookupError, err:
> +            succsets = obsolete.successorssets(self.repo, err.rev)
> +
> +            if not succsets:
> +                req.respond(HTTP_GONE, ctype)
> +
> +                return tmpl('error', error=err.message)
> +
> +            elif len(succsets) != 1:
> +                # TODO: changeset has divergent successors
> +                req.respond(HTTP_SERVER_ERROR, ctype)

divergent successors are not "server error" (my best guest for now would be "GONE")

> +
> +                return tmpl('error', error=err.message)
> +
> +            location = [req.url.rstrip('/')]
> +            location += req.form['cmd']
> +
> +            location.append(short(succsets[0][-1]))
> +
> +            if 'file' in req.form:
> +                location += req.form['file']
> +
> +            req.headers.extend([('Location', '/'.join(location))])
> +            req.respond(HTTP_MOVED_PERMANENTLY, ctype)
> +
> +            return tmpl('error', error=err.message)
> +
>         except (error.LookupError, error.RepoLookupError), err:
>             req.respond(HTTP_NOT_FOUND, ctype)
>             msg = str(err)
> diff --git a/mercurial/hgweb/webutil.py b/mercurial/hgweb/webutil.py
> --- a/mercurial/hgweb/webutil.py
> +++ b/mercurial/hgweb/webutil.py
> @@ -201,6 +201,9 @@ def cleanpath(repo, path):
> def changeidctx (repo, changeid):
>     try:
>         ctx = repo[changeid]
> +    except error.FilteredLookupError:
> +        raise
> +
>     except error.RepoError:
>         man = repo.manifest
>         ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
> diff --git a/tests/test-hgweb-commands.t b/tests/test-hgweb-commands.t
> --- a/tests/test-hgweb-commands.t
> +++ b/tests/test-hgweb-commands.t
> @@ -1385,7 +1385,7 @@ proper status for filtered revision
>   $ PATH_INFO=/rev/5; export PATH_INFO
>   $ QUERY_STRING='style=raw'
>   $ python hgweb.cgi #> search
> -  Status: 404 Not Found\r (esc)
> +  Status: 410 Gone\r (esc)
>   ETag: *\r (glob) (esc)
>   Content-Type: text/plain; charset=ascii\r (esc)
>   \r (esc)
> @@ -1399,7 +1399,7 @@ proper status for filtered revision
>   $ PATH_INFO=/rev/4; export PATH_INFO
>   $ QUERY_STRING='style=raw'
>   $ python hgweb.cgi #> search
> -  Status: 404 Not Found\r (esc)
> +  Status: 410 Gone\r (esc)
>   ETag: *\r (glob) (esc)
>   Content-Type: text/plain; charset=ascii\r (esc)
>   \r (esc)
> @@ -1498,6 +1498,47 @@ filtered '0' changeset
> 
> 
> 
> +Test obsolete redirection
> +
> +  $ cat > ../obs.py << EOF
> +  > import mercurial.obsolete
> +  > mercurial.obsolete._enabled = True
> +  > EOF
> +  $ echo '[extensions]' >> $HGRCPATH
> +  $ echo "rebase=" >> $HGRCPATH
> +  $ echo "obs=${TESTTMP}/obs.py" >> $HGRCPATH
> +  $ hg up 12
> +  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
> +  $ echo B > b; hg add b
> +  $ hg ci -m 6
> +
> +Use a rebase to obsolete r13, and test redirection
> +
> +  $ hg rebase -r 13 -d 11
> +  $ PATH_INFO=/rev/13; export PATH_INFO
> +  $ QUERY_STRING='style=raw'
> +  $ python hgweb.cgi #> search
> +  Status: 301 Moved Permanently\r (esc)
> +  ETag: *\r (glob) (esc)
> +  Location: /test/rev/0d601cf5c587\r (esc)
> +  Content-Type: text/plain; charset=ascii\r (esc)
> +  \r (esc)
> +  
> +  error: revision 13 is hidden
> +
> +Rebase r13 once more, testing divergent changesets
> +
> +  $ hg --hidden rebase -r 13 -d 9
> +  $ PATH_INFO=/rev/13; export PATH_INFO
> +  $ QUERY_STRING='style=raw'
> +  $ python hgweb.cgi #> search
> +  Status: 500 Internal Server Error\r (esc)
> +  ETag: *\r (glob) (esc)
> +  Content-Type: text/plain; charset=ascii\r (esc)
> +  \r (esc)
> +  
> +  error: revision 13 is hidden
> +
> 
> 
>   $ cd ..
> diff --git a/tests/test-obsolete.t b/tests/test-obsolete.t
> --- a/tests/test-obsolete.t
> +++ b/tests/test-obsolete.t
> @@ -747,7 +747,7 @@ check filelog view
>   $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'rev/68'
>   200 Script output follows
>   $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'rev/67'
> -  404 Not Found
> +  410 Gone
>   [1]
> 
> check that web.view config option:
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@selenic.com
> http://selenic.com/mailman/listinfo/mercurial-devel
Tim Delaney - Aug. 18, 2013, 3:18 a.m.
On 16 August 2013 09:57, Pierre-Yves David
<pierre-yves.david@ens-lyon.org>wrote:

>
> On 8 août 2013, at 09:48, Dan Villiom Podlaski Christiansen wrote:
>
> > # HG changeset patch
> > # User Dan Villiom Podlaski Christiansen  <danchr@gmail.com>
> > # Date 1375624663 -7200
> > #      Sun Aug 04 15:57:43 2013 +0200
> > # Node ID 5507e2af80f5e8026a054ce440c0c8958134eefe
> > # Parent  b1f7ae28c3e371a70ee363a4fbe5d50063a224fc
> > hgweb: handle obsolete changesets gracefully
> >
> > If the changeset has any successors, issue a 403 Moved Permanently;
> > otherwise we issue a 410 Gone. Please note that this is slightly
> > misleading for 'secret' changesets, as they may appear later on.
>
> You are going too fast here.
>
> 1) issue 410 for filtered changeset
> 1.1) (403 or 404 for secret ?)
> 2) issue a smarter message with potential link to successors (could
> include information about who and when it was rewritten)
> 3) use redirect (does not like the idea yet)


IMO 410 Gone should not be used for filtered changesets - 410 should be
considered to be a permanent condition. The only case I think a 410 would
be appropriate would be an obsolete changeset with no successors (this
situation could change, but that would be unusual).

If an obsolete changeset has multiple successors then I think 409 Conflict
would be appropriate since it is possible for the user to resolve the
conflict and resubmit.

For all other filtered changesets I think 403 or 404 is the most
appropriate response. 403 feels more appropriate to me, but does leak some
information about the filtered changeset that wouldn't be leaked by a 404.
OTOH I personally think that it would be useful to distinguish between a
filtered changeset and a non-existent changeset, so would favour 403.

Tim Delaney
Laurens Holst - Aug. 20, 2013, 9:53 a.m.
Op 18-08-13 05:18, Tim Delaney schreef:
> On 16 August 2013 09:57, Pierre-Yves David 
> <pierre-yves.david@ens-lyon.org 
> <mailto:pierre-yves.david@ens-lyon.org>> wrote:
>
>
>     On 8 août 2013, at 09:48, Dan Villiom Podlaski Christiansen wrote:
>
>     > # HG changeset patch
>     > # User Dan Villiom Podlaski Christiansen  <danchr@gmail.com
>     <mailto:danchr@gmail.com>>
>     > # Date 1375624663 -7200
>     > #      Sun Aug 04 15:57:43 2013 +0200
>     > # Node ID 5507e2af80f5e8026a054ce440c0c8958134eefe
>     > # Parent  b1f7ae28c3e371a70ee363a4fbe5d50063a224fc
>     > hgweb: handle obsolete changesets gracefully
>     >
>     > If the changeset has any successors, issue a 403 Moved Permanently;
>

The 403 status code is Forbidden.

You mean 301 Moved Permanently?

>     > otherwise we issue a 410 Gone. Please note that this is slightly
>     > misleading for 'secret' changesets, as they may appear later on.
>
>     You are going too fast here.
>
>     1) issue 410 for filtered changeset
>     1.1) (403 or 404 for secret ?)
>     2) issue a smarter message with potential link to successors
>     (could include information about who and when it was rewritten)
>     3) use redirect (does not like the idea yet)
>
>
> IMO 410 Gone should not be used for filtered changesets - 410 should 
> be considered to be a permanent condition. The only case I think a 410 
> would be appropriate would be an obsolete changeset with no successors 
> (this situation could change, but that would be unusual).
>
> If an obsolete changeset has multiple successors then I think 409 
> Conflict would be appropriate since it is possible for the user to 
> resolve the conflict and resubmit.
>
> For all other filtered changesets I think 403 or 404 is the most 
> appropriate response. 403 feels more appropriate to me, but does leak 
> some information about the filtered changeset that wouldn't be leaked 
> by a 404. OTOH I personally think that it would be useful to 
> distinguish between a filtered changeset and a non-existent changeset, 
> so would favour 403.

As long as it's 404 when the changeset is in the secret phase.

It must not be possible to identify whether a secret changeset with a 
certain hash exists.

~Laurens
Tim Delaney - Aug. 20, 2013, 10:29 p.m.
On 20 August 2013 19:53, Laurens Holst <laurens.nospam@grauw.nl> wrote:

>  Op 18-08-13 05:18, Tim Delaney schreef:
>
> On 8 août 2013, at 09:48, Dan Villiom Podlaski Christiansen wrote:
>>
>> > # HG changeset patch
>> > # User Dan Villiom Podlaski Christiansen  <danchr@gmail.com>
>> > If the changeset has any successors, issue a 403 Moved Permanently;
>>
>
> The 403 status code is Forbidden.
>
> You mean 301 Moved Permanently?
>

This had been commented on previously - didn't see any need for me to ;)

> > otherwise we issue a 410 Gone. Please note that this is slightly
>> > misleading for 'secret' changesets, as they may appear later on.
>>
>>  You are going too fast here.
>>
>> 1) issue 410 for filtered changeset
>> 1.1) (403 or 404 for secret ?)
>> 2) issue a smarter message with potential link to successors (could
>> include information about who and when it was rewritten)
>> 3) use redirect (does not like the idea yet)
>
>
>  IMO 410 Gone should not be used for filtered changesets - 410 should be
> considered to be a permanent condition. The only case I think a 410 would
> be appropriate would be an obsolete changeset with no successors (this
> situation could change, but that would be unusual).
>
>  If an obsolete changeset has multiple successors then I think 409
> Conflict would be appropriate since it is possible for the user to resolve
> the conflict and resubmit.
>
>  For all other filtered changesets I think 403 or 404 is the most
> appropriate response. 403 feels more appropriate to me, but does leak some
> information about the filtered changeset that wouldn't be leaked by a 404.
> OTOH I personally think that it would be useful to distinguish between a
> filtered changeset and a non-existent changeset, so would favour 403.
>
>
> As long as it’s 404 when the changeset is in the secret phase.
>
> It must not be possible to identify whether a secret changeset with a
> certain hash exists.
>

Yes - a secret changeset should always return 404 no matter what other
states apply. That includes an obsolete changeset with a single secret
successor - asking for the obsolete changeset should return 404 instead of
redirecting.

For other filtered changesets that are not covered by another case, I
prefer 403 (it exists, but you're not allowed to see it).

Tim Delaney
Pierre-Yves David - Sept. 3, 2013, 4:21 p.m.
On 20 août 2013, at 11:53, Laurens Holst wrote:

> Op 18-08-13 05:18, Tim Delaney schreef:
>> On 16 August 2013 09:57, Pierre-Yves David <pierre-yves.david@ens-lyon.org> wrote:
>> 
>> On 8 août 2013, at 09:48, Dan Villiom Podlaski Christiansen wrote:
>> 
>> > # HG changeset patch
>> > # User Dan Villiom Podlaski Christiansen  <danchr@gmail.com>
>> > # Date 1375624663 -7200
>> > #      Sun Aug 04 15:57:43 2013 +0200
>> > # Node ID 5507e2af80f5e8026a054ce440c0c8958134eefe
>> > # Parent  b1f7ae28c3e371a70ee363a4fbe5d50063a224fc
>> > hgweb: handle obsolete changesets gracefully
>> >
>> > If the changeset has any successors, issue a 403 Moved Permanently;
> 
> The 403 status code is Forbidden.
> 
> You mean 301 Moved Permanently?
> 
>> > otherwise we issue a 410 Gone. Please note that this is slightly
>> > misleading for 'secret' changesets, as they may appear later on.
>> 
>> You are going too fast here.
>> 
>> 1) issue 410 for filtered changeset
>> 1.1) (403 or 404 for secret ?)
>> 2) issue a smarter message with potential link to successors (could include information about who and when it was rewritten)
>> 3) use redirect (does not like the idea yet)
>> 
>> IMO 410 Gone should not be used for filtered changesets - 410 should be considered to be a permanent condition. The only case I think a 410 would be appropriate would be an obsolete changeset with no successors (this situation could change, but that would be unusual).
>> 
>> If an obsolete changeset has multiple successors then I think 409 Conflict would be appropriate since it is possible for the user to resolve the conflict and resubmit.
>> 
>> For all other filtered changesets I think 403 or 404 is the most appropriate response. 403 feels more appropriate to me, but does leak some information about the filtered changeset that wouldn't be leaked by a 404. OTOH I personally think that it would be useful to distinguish between a filtered changeset and a non-existent changeset, so would favour 403.
> 
> As long as it’s 404 when the changeset is in the secret phase.
> 
> It must not be possible to identify whether a secret changeset with a certain hash exists.

You may get the information from the obsolescence marker anyway.

if A (draft) is obsoleted by B (secret) the marker you'll get will have a reference to B.
Pierre-Yves David - Sept. 3, 2013, 6 p.m.
On 18 août 2013, at 05:18, Tim Delaney wrote:

> IMO 410 Gone should not be used for filtered changesets - 410 should be considered to be a permanent condition. The only case I think a 410 would be appropriate would be an obsolete changeset with no successors (this situation could change, but that would be unusual).
> 
> If an obsolete changeset has multiple successors then I think 409 Conflict would be appropriate since it is possible for the user to resolve the conflict and resubmit.
> 
> For all other filtered changesets I think 403 or 404 is the most appropriate response. 403 feels more appropriate to me, but does leak some information about the filtered changeset that wouldn't be leaked by a 404. OTOH I personally think that it would be useful to distinguish between a filtered changeset and a non-existent changeset, so would favour 403.

=== Core reply ===

I think you under-estimate the complexity of situation we can get. My point in the previous email was: "We need to get a better status and error message when stuff are filtered. When we have a better status/error we can think about special handling of situation".

It could be anything that fit generic filtering right. We could use 418 but you seems right, 403 request seem the way to go in that case (RFC content below for the record)
(special handling of secret may exist)

> 403 - FORBIDDEN: The server understood the request, but is refusing to fulfill it. Authorization will not help and the request SHOULD NOT be repeated. If the request method was not HEAD and the server wishes to make public why the request has not been fulfilled, it SHOULD describe the reason for the refusal in the entity. If the server does not wish to make this information available to the client, the status code 404 (Not Found) can be used instead.

(note: I'm not so happy to use 'FORBIDDEN' for obsolete changeset as there is no reason for not giving an ultimate way to retrieve them.)

=== Complexity example ===

Here is example of sightly different situation. I'm not telling that it's impossible to take the right decision for all of them. I'm pointing that we can't expect to embrace everything in one go.

Should the treat the same way:

- changeset obsolete with no successors ?
- changeset obsolete with an unknown successors ?
- changeset obsolete with a known secret successors ?

(read successors as: successors set in all sentences above)

Should the treat the same way:
- changeset have two possible successors set (diverse)
- changeset have a single successors set with two consecutive changesets (split)
- changeset have a single successors set with two changesets on different head (split + move)
Laurens Holst - Sept. 4, 2013, 8:28 a.m.
Op 03-09-13 20:00, Pierre-Yves David schreef:
> On 18 août 2013, at 05:18, Tim Delaney wrote:
>
>> IMO 410 Gone should not be used for filtered changesets - 410 should be considered to be a permanent condition. The only case I think a 410 would be appropriate would be an obsolete changeset with no successors (this situation could change, but that would be unusual).
>>
>> If an obsolete changeset has multiple successors then I think 409 Conflict would be appropriate since it is possible for the user to resolve the conflict and resubmit.
>>
>> For all other filtered changesets I think 403 or 404 is the most appropriate response. 403 feels more appropriate to me, but does leak some information about the filtered changeset that wouldn't be leaked by a 404. OTOH I personally think that it would be useful to distinguish between a filtered changeset and a non-existent changeset, so would favour 403.
> === Core reply ===
>
> I think you under-estimate the complexity of situation we can get. My point in the previous email was: "We need to get a better status and error message when stuff are filtered. When we have a better status/error we can think about special handling of situation".
>
> It could be anything that fit generic filtering right. We could use 418 but you seems right, 403 request seem the way to go in that case (RFC content below for the record)
> (special handling of secret may exist)
>
>> 403 - FORBIDDEN: The server understood the request, but is refusing to fulfill it. Authorization will not help and the request SHOULD NOT be repeated. If the request method was not HEAD and the server wishes to make public why the request has not been fulfilled, it SHOULD describe the reason for the refusal in the entity. If the server does not wish to make this information available to the client, the status code 404 (Not Found) can be used instead.
> (note: I'm not so happy to use 'FORBIDDEN' for obsolete changeset as there is no reason for not giving an ultimate way to retrieve them.)

Given this, why do we prevent access at all? I can think of plenty of 
use cases where there is a link to the original unamended changeset and 
you want that link to remain working and pointing to the exact same 
content. If only to illustrate the difference between the original and 
the amended changeset.

Wouldn’t it suffice to just have a big banner on top stating that the 
changeset is obsolete and replaced by changesets (...)? The user can 
then decide to follow the link (or links), and we avoid all kind of 
weirdness with “forbidden” responses or redirects that only work if 
there is one successor.

~Laurens

Patch

diff --git a/mercurial/hgweb/common.py b/mercurial/hgweb/common.py
--- a/mercurial/hgweb/common.py
+++ b/mercurial/hgweb/common.py
@@ -9,12 +9,14 @@ 
 import errno, mimetypes, os
 
 HTTP_OK = 200
+HTTP_MOVED_PERMANENTLY = 301
 HTTP_NOT_MODIFIED = 304
 HTTP_BAD_REQUEST = 400
 HTTP_UNAUTHORIZED = 401
 HTTP_FORBIDDEN = 403
 HTTP_NOT_FOUND = 404
 HTTP_METHOD_NOT_ALLOWED = 405
+HTTP_GONE = 410
 HTTP_SERVER_ERROR = 500
 
 
diff --git a/mercurial/hgweb/hgweb_mod.py b/mercurial/hgweb/hgweb_mod.py
--- a/mercurial/hgweb/hgweb_mod.py
+++ b/mercurial/hgweb/hgweb_mod.py
@@ -8,11 +8,13 @@ 
 
 import os
 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
+from mercurial import obsolete
+from mercurial.node import short
 from mercurial.templatefilters import websub
 from mercurial.i18n import _
 from common import get_stat, ErrorResponse, permhooks, caching
-from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
-from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
+from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST, HTTP_GONE
+from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_MOVED_PERMANENTLY
 from request import wsgirequest
 import webcommands, protocol, webutil, re
 
@@ -249,6 +251,33 @@  class hgweb(object):
 
             return content
 
+        except error.FilteredLookupError, err:
+            succsets = obsolete.successorssets(self.repo, err.rev)
+
+            if not succsets:
+                req.respond(HTTP_GONE, ctype)
+
+                return tmpl('error', error=err.message)
+
+            elif len(succsets) != 1:
+                # TODO: changeset has divergent successors
+                req.respond(HTTP_SERVER_ERROR, ctype)
+
+                return tmpl('error', error=err.message)
+
+            location = [req.url.rstrip('/')]
+            location += req.form['cmd']
+
+            location.append(short(succsets[0][-1]))
+
+            if 'file' in req.form:
+                location += req.form['file']
+
+            req.headers.extend([('Location', '/'.join(location))])
+            req.respond(HTTP_MOVED_PERMANENTLY, ctype)
+
+            return tmpl('error', error=err.message)
+
         except (error.LookupError, error.RepoLookupError), err:
             req.respond(HTTP_NOT_FOUND, ctype)
             msg = str(err)
diff --git a/mercurial/hgweb/webutil.py b/mercurial/hgweb/webutil.py
--- a/mercurial/hgweb/webutil.py
+++ b/mercurial/hgweb/webutil.py
@@ -201,6 +201,9 @@  def cleanpath(repo, path):
 def changeidctx (repo, changeid):
     try:
         ctx = repo[changeid]
+    except error.FilteredLookupError:
+        raise
+
     except error.RepoError:
         man = repo.manifest
         ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
diff --git a/tests/test-hgweb-commands.t b/tests/test-hgweb-commands.t
--- a/tests/test-hgweb-commands.t
+++ b/tests/test-hgweb-commands.t
@@ -1385,7 +1385,7 @@  proper status for filtered revision
   $ PATH_INFO=/rev/5; export PATH_INFO
   $ QUERY_STRING='style=raw'
   $ python hgweb.cgi #> search
-  Status: 404 Not Found\r (esc)
+  Status: 410 Gone\r (esc)
   ETag: *\r (glob) (esc)
   Content-Type: text/plain; charset=ascii\r (esc)
   \r (esc)
@@ -1399,7 +1399,7 @@  proper status for filtered revision
   $ PATH_INFO=/rev/4; export PATH_INFO
   $ QUERY_STRING='style=raw'
   $ python hgweb.cgi #> search
-  Status: 404 Not Found\r (esc)
+  Status: 410 Gone\r (esc)
   ETag: *\r (glob) (esc)
   Content-Type: text/plain; charset=ascii\r (esc)
   \r (esc)
@@ -1498,6 +1498,47 @@  filtered '0' changeset
   
   
 
+Test obsolete redirection
+
+  $ cat > ../obs.py << EOF
+  > import mercurial.obsolete
+  > mercurial.obsolete._enabled = True
+  > EOF
+  $ echo '[extensions]' >> $HGRCPATH
+  $ echo "rebase=" >> $HGRCPATH
+  $ echo "obs=${TESTTMP}/obs.py" >> $HGRCPATH
+  $ hg up 12
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo B > b; hg add b
+  $ hg ci -m 6
+
+Use a rebase to obsolete r13, and test redirection
+
+  $ hg rebase -r 13 -d 11
+  $ PATH_INFO=/rev/13; export PATH_INFO
+  $ QUERY_STRING='style=raw'
+  $ python hgweb.cgi #> search
+  Status: 301 Moved Permanently\r (esc)
+  ETag: *\r (glob) (esc)
+  Location: /test/rev/0d601cf5c587\r (esc)
+  Content-Type: text/plain; charset=ascii\r (esc)
+  \r (esc)
+  
+  error: revision 13 is hidden
+
+Rebase r13 once more, testing divergent changesets
+
+  $ hg --hidden rebase -r 13 -d 9
+  $ PATH_INFO=/rev/13; export PATH_INFO
+  $ QUERY_STRING='style=raw'
+  $ python hgweb.cgi #> search
+  Status: 500 Internal Server Error\r (esc)
+  ETag: *\r (glob) (esc)
+  Content-Type: text/plain; charset=ascii\r (esc)
+  \r (esc)
+  
+  error: revision 13 is hidden
+
 
 
   $ cd ..
diff --git a/tests/test-obsolete.t b/tests/test-obsolete.t
--- a/tests/test-obsolete.t
+++ b/tests/test-obsolete.t
@@ -747,7 +747,7 @@  check filelog view
   $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'rev/68'
   200 Script output follows
   $ "$TESTDIR/get-with-headers.py" --headeronly localhost:$HGPORT 'rev/67'
-  404 Not Found
+  410 Gone
   [1]
 
 check that web.view config option: