[PATCH 2 of 2 V2] commands: add --browser argument to `hg serve`

Gregory Szorc gregory.szorc at gmail.com
Sun Jul 17 16:01:16 EDT 2016

# HG changeset patch
# User Gregory Szorc <gregory.szorc at gmail.com>
# Date 1468785660 25200
#      Sun Jul 17 13:01:00 2016 -0700
# Node ID 61e63f5a6922f693c7ca4e7ed8c5c8e8a45aba2a
# Parent  a6ed7328eac4500637021b9afa065225fe1b90ed
commands: add --browser argument to `hg serve`

When D. Richard Hipp visited the Mozilla office several months ago
and was showing me all the awesomeness in his Fossil VCS, one of the
features that stood out to me was `fossil ui` starting a fully-featured
HTTP/HTML server including the web browser. It was so... elegant
to just type a command and get a web browser pointed to a local
HTTP server where you could interact with your VCS.

>From my own experience, when I run `hg serve` I frequently open a web
browser to navigate the started server. Today, I have to click on the
printed URL or copy and paste it into my browser to open it. While it
only takes a few seconds, this wastes my precious time.

This patch adds a --browser argument to `hg serve` to streamline the
process of opening a browser when starting a local HTTP/HTML server.

The argument only works when starting servers that run the HTTP/HTML

I wish "--browser" could exist as a boolean with an optional value so
bareword "--browser" would open the default browser (instead of
requiring an argument). However, it doesn't appear our option parser
allows this. This does undermine the utility of the argument a bit
(now you have to type extra characters). But, users can always install
an [alias] entry to type fewer characters, so this is only a minor

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -6439,16 +6439,17 @@ def root(ui, repo):
     Returns 0 on success.
     ui.write(repo.root + "\n")
     [('A', 'accesslog', '', _('name of access log file to write to'),
+    ('', 'browser', '', _('web browser to run')),
     ('d', 'daemon', None, _('run server in background')),
     ('', 'daemon-postexec', [], _('used internally by daemon mode')),
     ('E', 'errorlog', '', _('name of error log file to write to'), _('FILE')),
     # use string type, then we can check if something was passed
     ('p', 'port', '', _('port to listen on (default: 8000)'), _('PORT')),
     ('a', 'address', '', _('address to listen on (default: all interfaces)'),
     ('', 'prefix', '', _('prefix path to serve from (default: server root)'),
@@ -6485,22 +6486,31 @@ def serve(ui, repo, **opts):
     By default, the server logs accesses to stdout and errors to
     stderr. Use the -A/--accesslog and -E/--errorlog options to log to
     To have the server choose a free port number to listen on, specify
     a port number of 0; in this case, the server will print the port
     number it uses.
+    A web browser will be opened at the started server if the ``--browser``
+    argument contains the name of a browser to start. The special value
+    ``default`` will open the default browser as configured by your
+    operating system.
     Returns 0 on success.
     if opts["stdio"] and opts["cmdserver"]:
         raise error.Abort(_("cannot use --stdio with --cmdserver"))
+    if (opts['stdio'] or opts['cmdserver']) and opts['browser']:
+        raise error.Abort(_('cannot use --browser with --cmdserver or '
+                            '--stdio'))
     if opts["stdio"]:
         if repo is None:
             raise error.RepoError(_("there is no Mercurial repository here"
                                     " (.hg not found)"))
         s = sshserver.sshserver(ui, repo)
     if opts["cmdserver"]:
diff --git a/mercurial/hgweb/__init__.py b/mercurial/hgweb/__init__.py
--- a/mercurial/hgweb/__init__.py
+++ b/mercurial/hgweb/__init__.py
@@ -4,16 +4,17 @@
 # Copyright 2005 Matt Mackall <mpm at selenic.com>
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 from __future__ import absolute_import
 import os
+import webbrowser
 from ..i18n import _
 from .. import (
@@ -79,18 +80,41 @@ class httpservice(object):
             write = self.ui.write
         self.url = 'http://%s%s/%s' % (fqaddr, port, prefix)
         write(_('listening at %s (bound to %s:%d)\n') %
               (self.url, bindaddr, self.httpd.port))
         self.ui.flush()  # avoid buffering of status message
     def run(self):
+        self._maybestartbrowser()
+    def _maybestartbrowser(self):
+        browser = self.opts.get('browser')
+        if not browser:
+            return
+        # webbrowser.get() accepts empty value to indicate default browser.
+        if browser == 'default':
+            browser = None
+        try:
+            browser = webbrowser.get(browser)
+            # Text-mode browsers block the calling process, which interferes
+            # with our server.
+            if (not isinstance(browser, webbrowser.BackgroundBrowser) and
+                not getattr(browser, 'background', None)):
+                self.ui.warn(_('(cannot start text-mode browsers; '
+                               'continuing to start server)\n'))
+                return
+            browser.open_new_tab(self.url)
+        except webbrowser.Error as e:
+            self.ui.warn(_('(unable to start web browser: %s)\n') % e.args[0])
 def createservice(ui, repo, opts):
     # this way we can check if something was given in the command-line
     if opts.get('port'):
         opts['port'] = util.getport(opts.get('port'))
     alluis = set([ui])
     if repo:
         baseui = repo.baseui
diff --git a/tests/test-completion.t b/tests/test-completion.t
--- a/tests/test-completion.t
+++ b/tests/test-completion.t
@@ -149,16 +149,17 @@ Show the global options
 Show the options for the "serve" command
   $ hg debugcomplete --options serve | sort
+  --browser
@@ -214,17 +215,17 @@ Show all commands + options
   export: output, switch-parent, rev, text, git, nodates
   forget: include, exclude
   init: ssh, remotecmd, insecure
   log: follow, follow-first, date, copies, keyword, rev, removed, only-merges, user, only-branch, branch, prune, patch, git, limit, no-merges, stat, graph, style, template, include, exclude
   merge: force, rev, preview, tool
   pull: update, force, rev, bookmark, branch, ssh, remotecmd, insecure
   push: force, rev, bookmark, branch, new-branch, ssh, remotecmd, insecure
   remove: after, force, subrepos, include, exclude
-  serve: accesslog, daemon, daemon-postexec, errorlog, port, address, prefix, name, web-conf, webdir-conf, pid-file, stdio, cmdserver, templates, style, ipv6, certificate
+  serve: accesslog, browser, daemon, daemon-postexec, errorlog, port, address, prefix, name, web-conf, webdir-conf, pid-file, stdio, cmdserver, templates, style, ipv6, certificate
   status: all, modified, added, removed, deleted, clean, unknown, ignored, no-status, copies, print0, rev, change, include, exclude, subrepos, template
   summary: remote
   update: clean, check, date, rev, tool
   addremove: similarity, subrepos, include, exclude, dry-run
   archive: no-decode, prefix, rev, type, subrepos, include, exclude
   backout: merge, commit, no-commit, parent, rev, edit, tool, include, exclude, message, logfile, date, user
   bisect: reset, good, bad, skip, extend, command, noupdate
   bookmarks: force, rev, delete, rename, inactive, template

More information about the Mercurial-devel mailing list