← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~jugmac00/turnip:publish-docs-on-rtd into turnip:master

 

Jürgen Gmach has proposed merging ~jugmac00/turnip:publish-docs-on-rtd into turnip:master.

Commit message:
Publish documentation on Read the Docs

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~jugmac00/turnip/+git/turnip/+merge/415251
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~jugmac00/turnip:publish-docs-on-rtd into turnip:master.
diff --git a/.gitignore b/.gitignore
index 5698ab9..bc07593 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,5 @@ parts
 tags
 TAGS
 turnip/version_info.py
+.tox/
+docs/_build/
diff --git a/README.rst b/README.rst
index 3350719..b1644bc 100644
--- a/README.rst
+++ b/README.rst
@@ -11,210 +11,5 @@ the frontend to the storage layer.
 
 None of the Python interfaces here should be considered stable.
 
-
-Architecture
-============
-
-turnip's architecture is designed to maximise simplicity, scalability
-and robustness. Each server provides roughly one service, and an
-installation need only run the servers that it desires. Most servers
-eschew local state to ease horizontal scaling, and those that do have
-local state can replicate and/or shard it.
-
-There are two separate server stacks: pack and API. The pack stack
-communicates with Git clients via the pack protocol (git://), smart
-HTTP, or smart SSH. The HTTP and SSH frontends unwrap the tunneled pack
-protocol, and forward it onto the midends as a normal pack protocol
-connection. The separate HTTP API stack provides a programmatic remote
-interface to high-level read and write operations on the repositories
-
-
-Frontends:
- * Pack
- * Smart HTTP
- * Smart SSH
- * HTTP API
-
-Midends:
- * Pack virtualisation
- * API virtualisation
-
-Backends:
- * Pack
- * API
-
-
-Internal protocol
-=================
-
-turnip uses an extension of the Git pack protocol for most communication
-between its servers. The only change is that turnip requests can specify
-arbitrary named parameters, not just a hostname.
-
-The relevant part of the Git pack protocol's git-proto-request is
-represented in ABNF as follows::
-
-   git-proto-request = request-command SP pathname NUL [ host-parameter NUL ]
-   host-parameter = "host=" hostname [ ":" port ]
-
-turnip-proto-request alters it to this::
-
-   turnip-proto-request = request-command SP pathname NUL \*( param NUL )
-   param = param-name "=" param-value
-   param-name = \*( %x01-3C / %x3E-FF ) ; exclude NUL and =
-   param-value = \*%x01-FF ; exclude NUL
-
-The only additional parameters implemented today are
-'turnip-stateless-rpc' and 'turnip-advertise-refs', which are used by
-the smart HTTP server to proxy to the standard pack protocol.
-
-turnip implements one externally-visible extension: a
-'turnip-set-symbolic-ref' service that sets a symbolic ref (currently only
-'HEAD' is permitted) to a given target. This may be used over the various
-protocols (git, SSH, smart HTTP), requesting the service in the same way as
-the existing 'git-upload-pack' and 'git-receive-pack' services::
-
-   turnip-set-symbolic-ref-request = set-symbolic-ref-line
-                                     flush-pkt
-   set-symbolic-ref-line           = PKT-LINE(refname SP refname)
-
-The server replies with an ACK indicating the symbolic ref name that was
-changed, or an error message::
-
-   turnip-set-symbolic-ref-response = set-symbolic-ref-ack / error-line
-   set-symbolic-ref-ack             = PKT-LINE("ACK" SP refname)
-
-
-Internally, Turnip also implements an extension to create repositories:
-'turnip-create-repo'. It receives the new repository's pathname and the same
-authentication parameters used by the external interface. The authentication
-details are used to confirm/abort the repository creation on Launchpad.
-
-Development
-===========
-
-Setup
------
-
-Create a bionic container (optional)::
-
-        lxc launch ubuntu:bionic turnip-bionic
-
-(You may want to use a profile to bind-mount your home directory as well.)
-
-Run the following::
-
-        sudo add-apt-repository ppa:launchpad/ppa
-        sudo apt-get update
-        cat system-dependencies.txt dependencies-devel.txt charm/packages.txt | sudo xargs apt-get install -y --no-install-recommends
-        make bootstrap
-        mkdir -p /var/tmp/git.launchpad.test
-
-Running
--------
-
-Pack smart-http/ssh services can be started with:
-
-    make run-pack
-
-The HTTP API can be started with:
-
-   make run-api                  
-
-
-Running LP locally as a Git client to Turnip
---------------------------------------------
-
-Turnip container needs to be able to talk to xmlrpc-private.launchpad.test.
-
-In the Turnip container the hosts file needs to point to the LP container (x.x.x.x is the IP address of LP):
-
-	user@turnip-bionic:~/turnip$ cat /etc/hosts
-	127.0.0.1 localhost
-	x.x.x.x launchpad.test launchpad.test answers.launchpad.test archive.launchpad.test api.launchpad.test bazaar.launchpad.test bazaar-internal.launchpad.test blueprints.launchpad.test bugs.launchpad.test code.launchpad.test feeds.launchpad.test keyserver.launchpad.test lists.launchpad.test ppa.launchpad.test private-ppa.launchpad.test testopenid.test translations.launchpad.test xmlrpc-private.launchpad.test xmlrpc.launchpad.test
-	# The following lines are desirable for IPv6 capable hosts
-	::1 ip6-localhost ip6-loopback
-	fe00::0 ip6-localnet
-	ff00::0 ip6-mcastprefix
-	ff02::1 ip6-allnodes
-	ff02::2 ip6-allrouters
-	ff02::3 ip6-allhosts
-
-A basic test that can be performed by dropping into the Turnip container shell. Below exception is expected as Repository '1' did not exist when the RPC call was performed, it does show however that Turnip is able to resolve xmlrpc-private.launchpad.test and there is connectivity between LP and Turnip:
-	user@launchpad:~$ lxc exec turnip-bionic python3
-	...
-	>>> from xmlrpc.client import ServerProxy
-	>>> proxy = ServerProxy('http://xmlrpc-private.launchpad.test:8087/git')
-	>>> proxy.translatePath('1', 'read', {})
-	Traceback (most recent call last):
-	...
-	xmlrpclib.Fault: <Fault 290: "Repository '1' not found.">
-	>>> exit()
-	root@turnip-bionic:~#
-
-In your LP container the hosts file needs to point to the Turnip container (x.x.x.x is the IP address of Turnip):
-
-	x.x.x.x git.launchpad.test
-
-Then, also in your LP container edit ~/.gitconfig and add these lines, where USER is your Launchpad username:
-
-	[url "git+ssh://USER@xxxxxxxxxxxxxxxxxx/"]
-		insteadof = lptest:
-
-Create a new repository locally (user@launchpad:~/repo in LP container in below example) and push it to LP&Turnip:
-
-	user@launchpad:~/repo$ git remote add origin git+ssh://user@xxxxxxxxxxxxxxxxxx:9422/~user/+git/repo
-	user@launchpad:~/repo$ git push --set-upstream origin master
-	Counting objects: 3, done.
-	Writing objects: 100% (3/3), 231 bytes | 231.00 KiB/s, done.
-	Total 3 (delta 0), reused 0 (delta 0)
-	To git+ssh://git.launchpad.test:9422/~user/+git/repo
-	* [new branch]      master -> master
-	Branch 'master' set up to track remote branch 'master' from 'origin'.
-	user@launchpad:~/repo$ 
-
-
-The LP log for above push:
-
-	10.209.173.202 - "" "xmlrpc-private.launchpad.test" [16/Dec/2019:13:41:13 +0300] "POST /authserver HTTP/1.0" 200 1312 4 0.00622892379761 0.00250482559204 0.00320911407471 "Anonymous" "AuthServerApplication:" "" "Twisted/XMLRPClib"
-
-	2019-12-16T13:41:17 INFO lp.code.xmlrpc.git [request-id=057364e1-9e12-48c6-857d-a228c56d88c2] Request received: translatePath('~user/+git/repo', 'write') for 243674
-
-	2019-12-16T13:41:17 INFO lp.code.xmlrpc.git [request-id=057364e1-9e12-48c6-857d-a228c56d88c2] translatePath succeeded: {'writable': True, 'path': '5', 'trailing': '', 'private': False}
-	10.209.173.202 - "" "xmlrpc-private.launchpad.test" [16/Dec/2019:13:41:17 +0300] "POST /git HTTP/1.0" 200 899 21 0.0600020885468 0.00421810150146 0.0549690723419 "Anonymous" "GitApplication:" "" "Twisted/XMLRPClib"
-
-	2019-12-16T13:41:18 INFO lp.code.xmlrpc.git [request-id=057364e1-9e12-48c6-857d-a228c56d88c2] Request received: checkRefPermissions('5', ['refs/heads/master']) for 243674
-
-	2019-12-16T13:41:18 INFO lp.code.xmlrpc.git [request-id=057364e1-9e12-48c6-857d-a228c56d88c2] checkRefPermissions succeeded: [('refs/heads/master', ['create', 'push', 'force_push'])]
-	10.209.173.202 - "" "xmlrpc-private.launchpad.test" [16/Dec/2019:13:41:18 +0300] "POST /git HTTP/1.0" 200 880 10 0.0158808231354 0.00237107276917 0.0127749443054 "Anonymous" "GitApplication:" "" "Twisted/XMLRPClib"
-
-	2019-12-16T13:41:18 INFO lp.code.xmlrpc.git [request-id=2f4f61d3-8e58-4fd9-9d45-1949e08ad297] Request received: notify('5')
-
-	2019-12-16T13:41:18 INFO lp.code.xmlrpc.git [request-id=2f4f61d3-8e58-4fd9-9d45-1949e08ad297] notify succeeded
-	10.209.173.202 - "" "xmlrpc-private.launchpad.test" [16/Dec/2019:13:41:18 +0300] "POST /git HTTP/1.0" 200 588 7 0.0113499164581 0.00207781791687 0.00744009017944 "Anonymous" "GitApplication:" "" "Twisted/XMLRPClib"
-
-
-Your local LP user must exist in LP - created with "utilities/make-lp-user USER" - and have an ssh key in local LP.
-When adding the SSH key to LP if emails can't go out the SSH key addition will fail. 
-One possible workaround is to use Fakeemail: https://github.com/tomwardill/fakeemail
-
-It is recommended to install it in a virtual environment,
-e.g. via `pipx <https://pypa.github.io/pipx/>`_:
-
-	pipx install fakeemail
-	~/.local/bin/fakeemail  25 8082 0.0.0.0
-	Message stored for: root@localhost
-
-When creating and pushing new branches to LP with this local setup, the branches need to be scanned (data about the branch copied into the Launchpad database).
-On production, this happens via the magic of cron.
-Locally you can make it happen by running in your launchpad directory:
-
-    cronscripts/process-job-source.py IGitRefScanJobSource
-
-Now you have a fully working and up-to-date branch -- you should be able to look at the branch page in Launchpad, view the source in codebrowse, and so on.
-
-
-Deployment
-==========
-
-Turnip is deployed with its own set of charms.  See charm/README.
+Documentation can be found at
+https://turnip.readthedocs.io/en/latest/index.html
diff --git a/docs/architecture.rst b/docs/architecture.rst
new file mode 100644
index 0000000..f7b380b
--- /dev/null
+++ b/docs/architecture.rst
@@ -0,0 +1,31 @@
+Architecture
+============
+
+turnip's architecture is designed to maximise simplicity, scalability
+and robustness. Each server provides roughly one service, and an
+installation need only run the servers that it desires. Most servers
+eschew local state to ease horizontal scaling, and those that do have
+local state can replicate and/or shard it.
+
+There are two separate server stacks: pack and API. The pack stack
+communicates with Git clients via the pack protocol (git://), smart
+HTTP, or smart SSH. The HTTP and SSH frontends unwrap the tunneled pack
+protocol, and forward it onto the midends as a normal pack protocol
+connection. The separate HTTP API stack provides a programmatic remote
+interface to high-level read and write operations on the repositories
+
+
+Frontends:
+ * Pack
+ * Smart HTTP
+ * Smart SSH
+ * HTTP API
+
+Midends:
+ * Pack virtualisation
+ * API virtualisation
+
+Backends:
+ * Pack
+ * API
+ 
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..2a1e8c5
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,62 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'turnip'
+copyright = '2022, Canonical Ltd'
+author = 'Launchpad developers'
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'alabaster'
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = []
+
+# This is required for the alabaster theme
+# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
+html_sidebars = {
+    "**": [
+        "globaltoc.html",
+        "relations.html",  # needs 'show_related': True theme option to display
+        "searchbox.html",
+    ]
+}
diff --git a/docs/deployment.rst b/docs/deployment.rst
new file mode 100644
index 0000000..2b97597
--- /dev/null
+++ b/docs/deployment.rst
@@ -0,0 +1,5 @@
+Deployment
+==========
+
+Turnip is deployed with its own set of charms.
+See charm/README.md in the source tree.
diff --git a/docs/development.rst b/docs/development.rst
new file mode 100644
index 0000000..04218c0
--- /dev/null
+++ b/docs/development.rst
@@ -0,0 +1,163 @@
+Development
+===========
+
+Setup
+-----
+
+Create a bionic container (optional):
+
+.. code:: bash
+
+    lxc launch ubuntu:bionic turnip-bionic
+
+(You may want to use a profile to bind-mount your home directory as well.)
+
+Run the following:
+
+.. code:: bash
+
+        sudo add-apt-repository ppa:launchpad/ppa
+        sudo apt-get update
+        cat system-dependencies.txt dependencies-devel.txt charm/packages.txt | sudo xargs apt-get install -y --no-install-recommends
+        make bootstrap
+        mkdir -p /var/tmp/git.launchpad.test
+
+Running
+-------
+
+Pack smart-http/ssh services can be started with:
+
+.. code:: bash
+
+    make run-pack
+
+The HTTP API can be started with:
+
+.. code:: bash
+
+   make run-api
+
+
+Running LP locally as a Git client to Turnip
+--------------------------------------------
+
+Turnip container needs to be able to talk to xmlrpc-private.launchpad.test.
+
+In the turnip container the hosts file needs to point to the LP container
+(x.x.x.x is the IP address of LP):
+
+.. code:: bash
+
+    user@turnip-bionic:~/turnip$ cat /etc/hosts
+    127.0.0.1 localhost
+    x.x.x.x launchpad.test launchpad.test answers.launchpad.test archive.launchpad.test api.launchpad.test bazaar.launchpad.test bazaar-internal.launchpad.test blueprints.launchpad.test bugs.launchpad.test code.launchpad.test feeds.launchpad.test keyserver.launchpad.test lists.launchpad.test ppa.launchpad.test private-ppa.launchpad.test testopenid.test translations.launchpad.test xmlrpc-private.launchpad.test xmlrpc.launchpad.test
+    # The following lines are desirable for IPv6 capable hosts
+    ::1 ip6-localhost ip6-loopback
+    fe00::0 ip6-localnet
+    ff00::0 ip6-mcastprefix
+    ff02::1 ip6-allnodes
+    ff02::2 ip6-allrouters
+    ff02::3 ip6-allhosts
+
+A basic test that can be performed by dropping into the turnip container shell.
+Below exception is expected as Repository ``1`` did not exist when the RPC
+call was performed, it does show however that turnip is able to resolve
+``xmlrpc-private.launchpad.test`` and there is connectivity between LP and
+turnip:
+
+.. code:: bash
+
+	user@launchpad:~$ lxc exec turnip-bionic python3
+
+.. code:: python
+
+    ...
+    >>> from xmlrpc.client import ServerProxy
+    >>> proxy = ServerProxy('http://xmlrpc-private.launchpad.test:8087/git')
+    >>> proxy.translatePath('1', 'read', {})
+    Traceback (most recent call last):
+    ...
+    xmlrpclib.Fault: <Fault 290: "Repository '1' not found.">
+    >>> exit()
+    root@turnip-bionic:~#
+
+In your LP container the hosts file needs to point to the turnip container
+(x.x.x.x is the IP address of turnip):
+
+    x.x.x.x git.launchpad.test
+
+Then, also in your LP container edit ~/.gitconfig and add these lines,
+where USER is your Launchpad username:
+
+.. code:: bash
+
+    [url "git+ssh://USER@xxxxxxxxxxxxxxxxxx/"]
+        insteadof = lptest:
+
+Create a new repository locally (user@launchpad:~/repo in LP container in below
+example) and push it to LP&Turnip:
+
+.. code:: bash
+
+    user@launchpad:~/repo$ git remote add origin git+ssh://user@xxxxxxxxxxxxxxxxxx:9422/~user/+git/repo
+    user@launchpad:~/repo$ git push --set-upstream origin master
+    Counting objects: 3, done.
+    Writing objects: 100% (3/3), 231 bytes | 231.00 KiB/s, done.
+    Total 3 (delta 0), reused 0 (delta 0)
+    To git+ssh://git.launchpad.test:9422/~user/+git/repo
+    * [new branch]      master -> master
+    Branch 'master' set up to track remote branch 'master' from 'origin'.
+    user@launchpad:~/repo$ 
+
+
+The LP log for above push:
+
+.. code:: bash
+
+    10.209.173.202 - "" "xmlrpc-private.launchpad.test" [16/Dec/2019:13:41:13 +0300] "POST /authserver HTTP/1.0" 200 1312 4 0.00622892379761 0.00250482559204 0.00320911407471 "Anonymous" "AuthServerApplication:" "" "Twisted/XMLRPClib"
+
+    2019-12-16T13:41:17 INFO lp.code.xmlrpc.git [request-id=057364e1-9e12-48c6-857d-a228c56d88c2] Request received: translatePath('~user/+git/repo', 'write') for 243674
+
+    2019-12-16T13:41:17 INFO lp.code.xmlrpc.git [request-id=057364e1-9e12-48c6-857d-a228c56d88c2] translatePath succeeded: {'writable': True, 'path': '5', 'trailing': '', 'private': False}
+    10.209.173.202 - "" "xmlrpc-private.launchpad.test" [16/Dec/2019:13:41:17 +0300] "POST /git HTTP/1.0" 200 899 21 0.0600020885468 0.00421810150146 0.0549690723419 "Anonymous" "GitApplication:" "" "Twisted/XMLRPClib"
+
+    2019-12-16T13:41:18 INFO lp.code.xmlrpc.git [request-id=057364e1-9e12-48c6-857d-a228c56d88c2] Request received: checkRefPermissions('5', ['refs/heads/master']) for 243674
+
+    2019-12-16T13:41:18 INFO lp.code.xmlrpc.git [request-id=057364e1-9e12-48c6-857d-a228c56d88c2] checkRefPermissions succeeded: [('refs/heads/master', ['create', 'push', 'force_push'])]
+    10.209.173.202 - "" "xmlrpc-private.launchpad.test" [16/Dec/2019:13:41:18 +0300] "POST /git HTTP/1.0" 200 880 10 0.0158808231354 0.00237107276917 0.0127749443054 "Anonymous" "GitApplication:" "" "Twisted/XMLRPClib"
+
+    2019-12-16T13:41:18 INFO lp.code.xmlrpc.git [request-id=2f4f61d3-8e58-4fd9-9d45-1949e08ad297] Request received: notify('5')
+
+    2019-12-16T13:41:18 INFO lp.code.xmlrpc.git [request-id=2f4f61d3-8e58-4fd9-9d45-1949e08ad297] notify succeeded
+    10.209.173.202 - "" "xmlrpc-private.launchpad.test" [16/Dec/2019:13:41:18 +0300] "POST /git HTTP/1.0" 200 588 7 0.0113499164581 0.00207781791687 0.00744009017944 "Anonymous" "GitApplication:" "" "Twisted/XMLRPClib"
+
+
+Your local LP user must exist in LP - created with
+``utilities/make-lp-user USER`` - and have an ssh key in local LP.
+When adding the SSH key to LP if emails can't go out the SSH key addition will
+fail. 
+One possible workaround is to use Fakeemail:
+https://github.com/tomwardill/fakeemail
+
+It is recommended to install it in a virtual environment,
+e.g. via `pipx <https://pypa.github.io/pipx/>`_:
+
+.. code:: bash
+
+    pipx install fakeemail
+    ~/.local/bin/fakeemail  25 8082 0.0.0.0
+    Message stored for: root@localhost
+
+When creating and pushing new branches to LP with this local setup,
+the branches need to be scanned (data about the branch copied into the
+Launchpad database).
+On production, this happens via the magic of cron.
+Locally you can make it happen by running in your launchpad directory:
+
+.. code:: bash
+
+    cronscripts/process-job-source.py IGitRefScanJobSource
+
+Now you have a fully working and up-to-date branch.
+You should be able to look at the branch page in Launchpad,
+view the source in codebrowse, and so on.
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..c2a6823
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,26 @@
+.. turnip documentation master file, created by
+   sphinx-quickstart on Tue Feb  8 14:01:22 2022.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+turnip
+======
+
+turnip is a flexible and scalable Git server suite written in Python
+using Twisted.
+
+The various servers provide customisable virtual hosting, with flexible
+authentication and authorisation, and individual horizontal scaling from
+the frontend to the storage layer.
+
+None of the Python interfaces here should be considered stable.
+
+.. toctree::
+   :maxdepth: 2
+
+   self
+   architecture
+   development
+   internal_protocol
+   deployment
+
diff --git a/docs/internal_protocol.rst b/docs/internal_protocol.rst
new file mode 100644
index 0000000..c11a467
--- /dev/null
+++ b/docs/internal_protocol.rst
@@ -0,0 +1,45 @@
+Internal protocol
+=================
+
+turnip uses an extension of the Git pack protocol for most communication
+between its servers. The only change is that turnip requests can specify
+arbitrary named parameters, not just a hostname.
+
+The relevant part of the Git pack protocol's git-proto-request is
+represented in ABNF as follows::
+
+   git-proto-request = request-command SP pathname NUL [ host-parameter NUL ]
+   host-parameter = "host=" hostname [ ":" port ]
+
+turnip-proto-request alters it to this::
+
+   turnip-proto-request = request-command SP pathname NUL \*( param NUL )
+   param = param-name "=" param-value
+   param-name = \*( %x01-3C / %x3E-FF ) ; exclude NUL and =
+   param-value = \*%x01-FF ; exclude NUL
+
+The only additional parameters implemented today are
+``turnip-stateless-rpc`` and ``turnip-advertise-refs``, which are used by
+the smart HTTP server to proxy to the standard pack protocol.
+
+turnip implements one externally-visible extension: a
+``turnip-set-symbolic-ref`` service that sets a symbolic ref (currently only
+``HEAD`` is permitted) to a given target. This may be used over the various
+protocols (git, SSH, smart HTTP), requesting the service in the same way as
+the existing ``git-upload-pack`` and ``git-receive-pack`` services::
+
+   turnip-set-symbolic-ref-request = set-symbolic-ref-line
+                                     flush-pkt
+   set-symbolic-ref-line           = PKT-LINE(refname SP refname)
+
+The server replies with an ACK indicating the symbolic ref name that was
+changed, or an error message::
+
+   turnip-set-symbolic-ref-response = set-symbolic-ref-ack / error-line
+   set-symbolic-ref-ack             = PKT-LINE("ACK" SP refname)
+
+
+Internally, Turnip also implements an extension to create repositories:
+``turnip-create-repo``. It receives the new repository's pathname and the same
+authentication parameters used by the external interface. The authentication
+details are used to confirm/abort the repository creation on Launchpad.
diff --git a/setup.py b/setup.py
index fb7b2ea..d97e823 100755
--- a/setup.py
+++ b/setup.py
@@ -45,6 +45,9 @@ deploy_requires = [
     'envdir',
     'gunicorn',
     ]
+docs_requires = {
+    'sphinx',
+}
 
 setup(
     name='turnip',
@@ -73,7 +76,9 @@ setup(
     tests_require=test_requires,
     extras_require=dict(
         test=test_requires,
-        deploy=deploy_requires),
+        deploy=deploy_requires,
+        docs=docs_requires,
+    ),
     test_suite='turnip',
     entry_points="""\
     [paste.app_factory]
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..4f38868
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,9 @@
+[tox]
+env_list = docs
+
+[testenv:docs]
+description = Build documentation via Sphinx.
+basepython = python3
+extras = docs
+commands =
+    sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html