├── .gitattributes
├── .gitignore
├── .travis.yml
├── LICENSE
├── MANIFEST.in
├── README.rst
├── deploy_key.enc
├── docs
├── Makefile
├── _templates
│ ├── globaltocindex.html
│ ├── localtocindex.html
│ └── sidebarhelp.html
├── api.rst
├── changelog.rst
├── commandline.rst
├── conf.py
├── index.rst
├── make.bat
├── recipes.rst
├── releasing.rst
└── tests.rst
├── doctr
├── __init__.py
├── __main__.py
├── _version.py
├── common.py
├── local.py
├── tests
│ ├── __init__.py
│ ├── test_local.py
│ └── test_travis.py
└── travis.py
├── github_deploy_key_drdoctr_drdoctr_github_io.enc
├── setup.cfg
├── setup.py
├── test
└── versioneer.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | doctr/_version.py export-subst
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | env/
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | nosetests.xml
46 | coverage.xml
47 | *,cover
48 | .hypothesis/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 |
58 | # Flask stuff:
59 | instance/
60 | .webassets-cache
61 |
62 | # Scrapy stuff:
63 | .scrapy
64 |
65 | # Sphinx documentation
66 | docs/_build/
67 |
68 | # PyBuilder
69 | target/
70 |
71 | # IPython Notebook
72 | .ipynb_checkpoints
73 |
74 | # pyenv
75 | .python-version
76 |
77 | # celery beat schedule file
78 | celerybeat-schedule
79 |
80 | # dotenv
81 | .env
82 |
83 | # virtualenv
84 | venv/
85 | ENV/
86 |
87 | # Spyder project settings
88 | .spyderproject
89 |
90 | # Rope project settings
91 | .ropeproject
92 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | sudo: false
4 | env:
5 | matrix:
6 | - DOCS="true"
7 | - TESTS="true"
8 | global:
9 | # Deploy key for drdoctr/doctr
10 | - secure: "DnMBexSob9FJwOaIE3OaA2szQ3e+NwpFv3imT3UyAIbxxxXGCHLAwQdeJrB7ivDJPUdEBUvbKBdKo8O4GdiUeTJHZvmr2cT+k0iCj5fkEaLyCqwtfXmQWP5hjQ9RKshqsyHgfkooo2ItLDEnZD/XTonnIzohUQobWzOTW6W3a0A+FRNq8cIvZIslmX8xPF7wxisBH8XiZad2c+HbSWIIJcfGbBN+k+KzvqqG2LNXX3s9iP27h+Fgluk48q0bSbglGAe/F87z2ZUOACCFFT2VhVn8yNrtaZyWfQhR2jFPUqCUffIyNozH2SYXooLuY0DqzEH7rIgCZDMF3homSvTITfeDMYxnW7LefGqrG7eh4rpE9w4Kdenuqcf1UbRBv0dy1jun+wAgUlQ2ZGqBG1dTeZRKlabrBHYDjNReRKdcqqYQmEIYc/S3G4jYOxzY4Eo4sFLeOKqcZCPigGE3pEt4oq1OBwIA5AuT8k28Sm6BneBibe0JgxpwVIbOOBkiHcgjrXxF/q9gp7B3pFSeQlNNIhEzIbHObiPQHwh++wGpccqmtD0FwenhhbBGDuAvLX+GFmjKve+8k/9+k41uvxP9FhNNmtYvozzYcJ+8BvXybU3kodJZfMw7+cIdwIW4x0s/8qJCiQhABo9Bpgx4Uw7y2ljb/K9kd6Kv8k62CsM7oMg="
11 | # Deploy key for drdoctr/drdoctr.github.io
12 | - secure: "miMtYnqGtt26LT3Tr23OwHOST2NGEJZsng5oMSP26lRv81ZQZ+VsFhEcjptUR1KfQia05t2viVTuyedZIgZe23dn3KDEm6Lqp8COAERe6jnW3/qZdHyD1zC3ddaku0dS4LVnk7jaRsjpu1mSdODxGZ9pCKA861uDDKk8DUeWx40sOte6MYL7OS38HavPCGJI0s4OL9eVqN++TAiZ9ts+Wa1O0M5wEkxaR4ES5cmzC0TLErKCnjyzy4aVU9+ykU43qVncq7w0857TSicBhh/zp0/6mvrBzv3lVCC0vUUiPrca0cn6mNTLggHFiur/BCfVAuEE5MPsZlW79jqtsYb+3lr45ELx22zUkZtXJtYOhlsA9AIG42iRMuOilt1Db0aVvlOAisvUZtSN1X314zYnqNLgCf32VpIsFFCuTBmW99UfvXpqOt0zRgnD50PEM4vDVO1L5HslNAmnAfa/l/jAFWD3xVMd+mSik2kVZo1i3ntjRiMjs6CGJulVS/psKSG6umTFyV1v9MCRHKLtfPatXVnlQ1OtdtP6w1DDsmWSxu+lHGmcSzcBqBzq6PrIRv3JmVghEloQVhYDaeb1/IXJu7l4uAV2M8vXaQU2syDbYpC/mP/1WNt0zFh0rHHRvhfqXylW65h7jxqt3mDMJq0IkaGCw3hXo63h4hjcZG+E7C4="
13 | python:
14 | - 3.5
15 | - 3.6
16 |
17 | matrix:
18 | include:
19 | - python: 3.7
20 | dist: xenial
21 | sudo: true
22 | env:
23 | - DOCS="true"
24 | - python: 3.7
25 | dist: xenial
26 | sudo: true
27 | env:
28 | - TESTS="true"
29 |
30 | install:
31 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh;
32 | - bash miniconda.sh -b -p $HOME/miniconda
33 | - export PATH="$HOME/miniconda/bin:$PATH"
34 | - hash -r
35 | - conda config --set always_yes yes --set changeps1 no
36 | - conda config --add channels conda-forge # For sphinxcontrib.autoprogram
37 | - conda update -q conda
38 | - conda info -a
39 | - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION requests cryptography sphinx pyflakes sphinxcontrib-autoprogram pytest sphinx-issues pyyaml
40 | - source activate test-environment
41 |
42 | script:
43 | - set -e
44 | - set -x
45 | # Note, the commands below need --sync to actually sync, to override sync: False below. Also, every deploy to the main repo needs to have --key-path deploy_key.enc.
46 | - |
47 | if [[ "${DOCS}" == "true" ]]; then
48 | cd docs;
49 | make html;
50 | cd ..;
51 | python -m doctr deploy --sync --key-path deploy_key.enc .;
52 | python -m doctr deploy --sync --key-path deploy_key.enc --gh-pages-docs docs;
53 | python -m doctr deploy --sync --key-path deploy_key.enc --no-require-master --built-docs docs/_build/html "docs-$TRAVIS_BRANCH" --command "echo test && ls && touch docs-$TRAVIS_BRANCH/test-file && git add docs-$TRAVIS_BRANCH/test-file"
54 | echo `date` >> test
55 | python -m doctr deploy --key-path deploy_key.enc --no-require-master docs;
56 | # Test syncing a tracked file with a change
57 | python -m doctr deploy --sync --key-path deploy_key.enc stash-test/ --built-docs test;
58 | # Test syncing a single file
59 | echo `date` >> test-file
60 | python -m doctr deploy --sync --key-path deploy_key.enc . --built-docs test-file;
61 | # Test deploy branch creation. Delete the branch gh-pages-testing on drdoctr/doctr whenever you want to test this.
62 | python -m doctr deploy --sync --key-path deploy_key.enc --no-require-master --deploy-branch gh-pages-testing docs;
63 | # Test pushing to .github.io
64 | python -m doctr deploy --sync --no-require-master --deploy-repo drdoctr/drdoctr.github.io "docs-$TRAVIS_BRANCH";
65 | # Actual docs deployment
66 | python -m doctr deploy --sync --deploy-repo drdoctr/drdoctr.github.io .;
67 | # GitHub Wiki deploy
68 | echo "This page was automatically deployed by doctr on $(date)" > deploy-test.md;
69 | python -m doctr deploy --sync --key-path deploy_key.enc --no-require-master --deploy-repo drdoctr/doctr.wiki . --built-docs deploy-test.md;
70 | # Build on tags
71 | python -m doctr deploy --sync --key-path deploy_key.enc "tag-$TRAVIS_TAG" --build-tags --branch-whitelist;
72 | # Test --branch-whitelist
73 | python -m doctr deploy --sync --key-path deploy_key.enc "branch-whitelist" --branch-whitelist branch-whitelist;
74 | # Test --exclude
75 | python -m doctr deploy --sync --key-path deploy_key.enc "exclude-test" --built-docs docs/_build/html --exclude tests.html;
76 | # Test syncing lots of files
77 | mkdir -p lots-of-files-test;
78 | python -c "for i in range(10000): open('lots-of-files-test/test-%s' % i, 'w')";
79 | python -m doctr deploy --sync --key-path deploy_key.enc lots-of-files-test --built-docs lots-of-files-test;
80 | fi
81 | - if [[ "${TESTS}" == "true" ]]; then
82 | pyflakes doctr;
83 | py.test doctr -v -rs;
84 | fi
85 |
86 | doctr:
87 | require-master: true
88 | sync: False
89 | lubalubadubdub: False
90 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Aaron Meurer, Gil Forsyth
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include versioneer.py
2 | include doctr/_version.py
3 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Doctr
2 | =====
3 |
4 | A tool for automatically deploying docs from Travis CI to GitHub pages.
5 |
6 | Doctr helps deploy things to GitHub pages from Travis CI by managing the
7 | otherwise complicated tasks of generating, encrypting, managing SSH deploy
8 | keys, and syncing files to the ``gh-pages`` branch. Doctr was originally
9 | designed for documentation, but it can be used to deploy any kind of website
10 | to GitHub pages that can be built on Travis CI. For example, you can use Doctr
11 | to deploy a `blog
12 | `_
13 | or website that uses a `static site generator `_.
14 |
15 | Contribute to Doctr development on `GitHub
16 | `_.
17 |
18 | Installation
19 | ------------
20 |
21 | Install Doctr with pip
22 |
23 | .. code::
24 |
25 | pip install doctr
26 |
27 | or conda
28 |
29 | .. code::
30 |
31 | conda install -c conda-forge doctr
32 |
33 | **Note that Doctr requires Python 3.5 or newer.**
34 |
35 | Usage
36 | -----
37 |
38 | Run Doctr configure
39 | ~~~~~~~~~~~~~~~~~~~
40 |
41 | First use Doctr to generate the necessary key files so that travis can push
42 | to your gh-pages (or other) branch.
43 |
44 | Run
45 |
46 | .. code::
47 |
48 | doctr configure
49 |
50 | and enter your data. You will need your GitHub username and password, and the
51 | repo organization / name for which you want to build the docs.
52 |
53 | **Note**: That repo should already be set up with Travis. We recommend enabling
54 | `branch protection `_
55 | for the ``gh-pages`` branch and other branches, as the deploy key
56 | used by Doctr has the ability to push to any branch in your repo.
57 |
58 | Edit your travis file
59 | ~~~~~~~~~~~~~~~~~~~~~
60 |
61 | Doctr will output a bunch of text as well as instructions for next steps. You
62 | need to edit your ``.travis.yml`` with this text. It contains the secure key
63 | that lets travis communicate with your GitHub repository, as well as the
64 | code to run (in ``script:``) in order to build the docs and deploy Doctr.
65 |
66 | Your ``.travis.yml`` file should look something like this:
67 |
68 | .. code:: yaml
69 |
70 | # Doctr requires python >=3.5
71 | language: python
72 | python:
73 | - 3.6
74 |
75 | # This gives Doctr the key we've generated
76 | sudo: false
77 | env:
78 | global:
79 | secure: ""
80 |
81 | # This is the script to build the docs on travis, then deploy
82 | script:
83 | - set -e
84 | - pip install doctr
85 | - cd docs
86 | - make html
87 | - cd ..
88 | - doctr deploy . --built-docs path/to/built/html/
89 |
90 | See `the travis config file
91 | `_ used by Doctr itself for example.
92 |
93 | You can deploy to a different folder by giving it a different path in the call
94 | to ``deploy``. E.g., ``doctr deploy docs/``.
95 |
96 | If you don't already have a gh_pages branch Doctr will make one for you.
97 |
98 | .. warning::
99 |
100 | Be sure to add ``set -e`` in ``script``, to prevent ``doctr`` from running
101 | when the docs build fails.
102 |
103 | Put ``doctr deploy .`` in the ``script`` section of your ``.travis.yml``. If
104 | you use ``after_success``, it will `not cause
105 | `_
106 | the build to fail.
107 |
108 | Commit your new files and build your site
109 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
110 |
111 | ``doctr configure`` will create a new file that contains your key. Commit this as
112 | well as the changes to ``.travis.yml``. Once you push to GitHub, travis should
113 | now automatically build your documentation and deploy it.
114 |
115 | Notes
116 | -----
117 |
118 | **Doctr requires Python 3.5 or newer.** Be sure to run it in a
119 | Python 3.5 or newer section of your build matrix. It should be in the same
120 | build in your build matrix as your docs build, as it reuses that.
121 |
122 | **Doctr does not require Sphinx.** It will work with deploying anything to
123 | GitHub pages. However, if you do use Sphinx, Doctr will find your Sphinx
124 | docs automatically (otherwise use ``doctr deploy . --built-docs ``).
125 |
126 | FAQ
127 | ---
128 |
129 | - **Why did you build this?**
130 |
131 | Deploying to GitHub pages from Travis is not amazingly difficult, but it's
132 | difficult enough that we wanted to write the code to do it once. We found
133 | that Travis docs uploading scripts are cargo culted and done in a way that
134 | is difficult to reproduce, especially the do-once steps of setting up keys.
135 | The ``doctr configure`` command handles key generation automatically, and
136 | tells you everything you need to do to set Doctr up. It is also completely
137 | self-contained (it does not depend on the ``travis`` Ruby gem). The ``doctr
138 | deploy`` command handles key decryption (for deploy keys) and hiding tokens
139 | from the command output (for personal access tokens).
140 |
141 | Furthermore, most Travis deploy guides that we've found recommend setting up
142 | a GitHub personal access token to push to GitHub pages. GitHub personal
143 | access tokens grant read/write access to all public GitHub repositories for
144 | a given user. A more secure way is to use a GitHub deploy key, which grants
145 | read/write access only to a single repository. Doctr creates a GitHub deploy
146 | key by default (although the option to use a token exists if you know what
147 | you are doing).
148 |
149 | - **Why not Read the Docs?**
150 |
151 | Read the Docs is great, but it has some limitations:
152 |
153 | - You are limited in what you can install in Read the Docs. Travis lets you
154 | run arbitrary code, which may be necessary to build your documentation.
155 |
156 | - Read the Docs deploys to readthedocs.io. Doctr deploys to GitHub pages.
157 | This is often more convenient, as your docs can easily sit alongside other
158 | website materials for your project on GitHub pages.
159 |
160 | In general, you should already be building your docs on Travis anyway (to
161 | test that they build), so it seems natural to deploy them from there.
162 |
163 | - **Why does Doctr require Python 3.5 or newer?**
164 |
165 | There are several language features of Python that we wanted to make use of
166 | that are not available in earlier versions of Python, such as `keyword-only
167 | arguments `_,
168 | `subprocess.run
169 | `_, and
170 | `recursive globs `_. These
171 | features help keep the Doctr code cleaner and more maintainable.
172 |
173 | If you cannot build your documentation in Python 3, you will need to
174 | install Python 3.6 in Travis to run Doctr.
175 |
176 | - **Is this secure?**
177 |
178 | Doctr creates an encrypted SSH deploy key, which allows any Travis build on
179 | your repo to push to the deploy repo. The deploy key is encrypted using
180 | `Fernet encryption from the Python cryptography module
181 | `_. The Fernet key is then
182 | encrypted to a secure environment variable for Travis using the `Travis
183 | public key `_.
184 |
185 | Travis does not make secure environment variables available to pull requests
186 | builds. Furthermore, Doctr itself does not push from any branch other than
187 | ``master`` by default, although this :ref:`can be changed `.
188 |
189 | By default, Doctr uses deploy keys, but it can also use a GitHub
190 | personal access token, using the ``--token`` flag. However, this is not
191 | recommended, as a GitHub personal access token grants access to your entire
192 | account, whereas a deploy key only grants push access only to a single
193 | repository.
194 |
195 | Both Doctr and Travis CI itself take measures to prevent the private
196 | encryption key from leaking in the build logs.
197 |
198 | At any time, you can revoke the deploy key created by Doctr by going to the
199 | deploy key settings for the repository in GitHub at
200 | :samp:`https://github.com/{org}/{repo}/settings/keys`. Personal access
201 | tokens can be revoked at `https://github.com/settings/tokens
202 | `_. If you revoke a key, you will need
203 | to rerun ``doctr configure`` to generate a new one to continue using Doctr.
204 |
205 | - **Can Doctr do X?**
206 |
207 | See the :ref:`recipes` page for many common use case recipes for Doctr.
208 | Doctr supports virtually anything that involves pushing from Travis CI to
209 | GitHub automatically.
210 |
211 | - **I would use this, but it's missing a feature that I want.**
212 |
213 | Doctr is still very new. We welcome all `feature requests
214 | `_ and `pull requests
215 | `_.
216 |
217 | - **Why is it called Doctr?**
218 |
219 | Because it deploys **doc**\ umentation from **Tr**\ avis. And it makes you
220 | feel good.
221 |
222 | Projects using Doctr
223 | --------------------
224 |
225 | - `SymPy `_
226 |
227 | - `conda `_
228 |
229 | - `doctr `_
230 |
231 | - `PyGBe `_
232 |
233 | - `xonsh `_
234 |
235 | - `regro-cf-autotick-bot `_
236 |
237 | - `XPD stack `_
238 |
239 | - `Spyder IDE `_
240 |
241 | Are you using Doctr? Please add your project to the list!
242 |
--------------------------------------------------------------------------------
/deploy_key.enc:
--------------------------------------------------------------------------------
1 | gAAAAABazUKOfdJyRuCv-2DMx9rEMIzjhdSAr_IH9ud913ZRpcAm5HVvDlbdAJNboZ37wy9quwU_m3W5WBy-_PxyLD2TaEX6cTHB9mfShAOx8qzBheunwN5UBHd54Zd3tm7KXV9vbbckkKtQRhhy2WpjQ72VlVl-iU_VubTFJzEQKbKy7pSM8sW098Eta06eF0SBlO9hdmTjijZiqnGzC_uExkOt4mHlg0_oKLiHLt1APTaWRaNIrAtFL-TJwbAhDID4dGtzJc7uD2gHSLaut9TPGFcK86hmpTTQ9w8P4VQv6ez-OpvDyidnq8OwltZirPhwh0fgNQ0paFjm2VGKDUP3N3-E31N7KIdUYyvp3W4qz2pGLKFulP7yO9dC-bnTw3liTlds-4JBIjKVLlBY4-o4ZKzEQeIrjSnv04VQaA7v0hF0IvSusE6i-KbYS0EXV4eX7wSum6EdX_zJxVXF1SbWSaPKbCZ3XJbsroqtvxVka-um2MxrboKqWvtSVgGtQNrxtLgZ2vgB3D4idFRW8RlEtL6Y1OuBNNdoGZxO-H1Epp8aWxcRzh04LQtjnPR-Jpn4EBn4CqZ6vj05Qs6wFsg3j9vrRcGvc9nsHNFT4FyuVgBKHSh_9Zz1icR-SlBS2p8zlNhsT-PwxhqSWJ_u1Oqwo1rXmDi1ZyZDlT5XcE_ruCxC1TeRtLIfooXcygmBxw2bLW3wl6Qqi5uU1SJ6J-UTFwkxYdRsZE-gdceD9CLqE8bA3EBeEjGVBKl1MakaxUga1TKN8I7w900lbmeQH8i-G2gFb6HIcvytMgoIPlZKIrP4gRU406kHQcMhJ_RrEYzgkM1K7-71N4Zo5JbadEmhPE70LcCHaEmjhg3-2qIOe2_om9IM68GnvTaOdyuO_vKCXPgsOSjMJ91eSF_EjNg65E0pAJLIUh4VTIApCcWi_mg0jf2WzfNdz-uRQkkBq7yH2B_hBdh9rUWQHSvAVhEVCRg4jUBt8-N_0P2hjyeNqJkM8aG912ikeSnzuJjmb1xY-gJYNNyqCMMy9JGDUBLaWIb6ZBicoXXg3HGtMTyuCLlv7gXJH2ZvXApwhTqaBxSs0UYDAG85Q8i9uNC1ltnVIASV3lVxtf9SoXhfQD0YcZQ3HlQwZpIB2vtlnym46Zr5Ubef4Lrk8dJOPTw74x9tgLpfRRf0wOMm6R78Mzt8nvwpixaHHICtiAvaDz0cBAFcqd-ip0eDTEDaUznITD748LtyxhSEma0YN6aU-BMMYHlR25R_dRy7XEeRqVgWgdUx0SwPJPIpDP2X9ru4ble7avur8_RmCBQ1njxgvwoxfphxR6SwRXzCU90H3r18ZjrOvR5DsPN7jG6SmTpDlwwYfsvgz_ueqoZ55YW5M_IsXdPPcUv6rlKXiFal1flWugdkVxpUQCy5767BCLvBcjPS-ctB4n4LWIy2kIkm8-wQz2u3Fq8dH06cX0nL0mW635zD_jtJNjoCEDa3Jn-K-pDvbuvMHwacMVuhNSTnODAG1qQfwEuQBK5Y6gwovwPpI-mOXztCU81ANkfqRSzvfsWVGpDUOeCkwv4TwlJYGhZ2adYpsRwOYJ47VOw4PMp8koDlQihfamwegEXB5RP9IYvmHsvoxdSx9iWMiz42BXJsv0IuOJm6M8w1o10MSkVCx6HfsVd48JAdnLaKNZAEeVLNFXKx3C5j4NEw7nzbxRJEFJMJUN27nW8lYvTf-FpooUtbE0Dfb1_ONEkOHxrLViNOrWq3RaFfqZAwQx-lBoiOem1Dz-8hg9aHh6M7-LdQN6Dn6OM8thxAr8cpgFgo00Dc3w8wLQ8MPU6kYNOXZHt08sTfwxtT7Usp590nZs2WwDin1gV7ECfGPP6uAP2pFSk0WzBKeYbk_ioDa5bENyhugKMFQKhOtNiJeuzf34FhGyscD7jTw9Q0iSbiTccPWavPeMow6zEqz8GQkXlfPs0bdd_DZkLqxDJ3A5CHzvUuBn-mndc7AI8OnPMOo0GubweFNUF9kjOL_4JckJVBpE2OAEQP71oCP0rU7MQmKTb3O5j1b4NeWAuWTBlevcX9CcH0-7pe2Rb-AH8CV0JeGud59-X6auYRXle0ZABeev1mvp3Jp1F9Cp7JtilgKBbe1OprZ0XV7BxQIFFwiWPs_zcAMXrOFG9t8q2bkk8l-WQXyGV6P7IQz4d9uWzEXja1IpxY46vL3BNbbLT7A4KE_1CEejk4mX0S9JE6YmoBobKk6AScRkUayMs1Fl8oBHfBgSnlWwjwUV7fQGVAvAPR5sA-CZdL7t8tuKg0hXvZQL1GtE36ewu4bWyDHJ1xFrS0mVr9RIxhfUgmM5CDs2gjeWlTVhS8UcECryI1Gm4VS6nflwMiAm3ynlSKjp0LaulZXxSpZqfX3AGibcV0cBUuHdzVYwM2cZ7tcKv6HAk28_al60ysWl2YJ0URY5ER7CahpdQD7lh_wQ9XbKqwwLPbCUtN4ZVCNnP8VIpPv-72LunTR1EPD6K3_qp1GrQ0KKCUm0BzajyyBlD0N4sU0gh6DtelikXOtvysBtBC72N7UKJZ5ordpyTYPjWvkKzdeVu1AK1DMq4nBAYKc0428aY6oGM19N9VLJ5Xa9e8AyvyVdOoLa1k_GcSZ4Mm-CoKaedvlvqRbROn6pCXFXs1gHBNOhP63IpEupT1Z8CZ7_nO-gfv4KwW6znHWeInjlIATl2XRCbJb6QVOrl6MHGu0sH-N6oJQhHufrPZT9ITtOug21RxWC4SLPj50bYMI_Xyn6ObCD9FFehyPxkOEX09f0bUAG23PJVVlzdr9slPc7xpZkAryAfchVpgGW1kA-6-VXryk8fzNbzBEGbHnKkP0B6jq-pgv13utTXsAI_Rjdpbx8MJIgBdZKmIZXH8zAqOhhP3e01K8LVke8RO7II7IRIVVwF174aqESip-OJtTJvbO9KRPhZxyFby-pe_9L9jlYq9ytqLPP4s-863sxG3G2zbD31d2YiPfPRODWCBHNZ1SWBZKRWqyZWVZyBDopb7AUyNxjMJjf9kjkf5emZrZzzXoQqMyOpf5yjmgwrhFjZnSf7irQb-otak5tPYq5d6wIN7E6W9O95GIyUOY6Hf9pJ8c0C-eX67cB5mRnM07Pjvq7YZJrzttP0r5Sj66Yajhv9VOVHf9uUIGDaJwU7NPmuDYMeXk2M42K__dd-8an3-8y7iiDwrnYBnr2yQOIa934_jBxe63xYFzDWfZhVbS5hbsE2pZPkbfj6iJY3BgzPhInBcCEvIsbgk4GNuB917ejN7iay__V5xyBaoIfkJCSVcYo6iAyuxoGmL0kQfFLGQVE7NR6WDGJVMuX4ghsVcDnh4KoeBryDd99C2s2e-OCowSCzo_f4ZruQLFVqzkrEKQrhjg1ZyDnJL2rfMi2EM73umpmtb4kDT2EagHi7IwzVd73AfYe4034MVhwEHDCzJE1yZ15w0F1_zGM_GbWvomp46cbiR8A6Gr3SsOsuqrSteFeAVizw5TUx82OqrIQVWONAj6bdjjIlVOd-UvMjcUcejdjFg551Nl6HSFT3w2iIdXNH2-mJpGL4eghTmYtqNmUL5V-k3Ow2mhMOrDx08om6GUI1rPprsVwYL1SplZP5xml4CKN7hiHkgyw9faqUJhjYN9-ip1hg90OPkCedG-51MifLHe7MMnsp2xbSmOQ8H0G3mnaIMo_VWlRe7nTGAJy6x9UmR0WZt0hD944ROxu-Me28d2ic4mKzObawy7rvxv2Nfdp7vMq2YxN5Ymp7SEGkv-bkGntzjcebBmiJQ9BxsLl5CQ4_qjR9Fx0Uri6NspkGxz3GZmO1LGYJ3rpsRcOg3ZwLvPavt_l_fjk_oR8OPC7tpZ4PwQYwocFAjTU11vdEmipwiVCX05YPdyCp5B2jjd--IBolzmcxsIGX8vOHfzm1i9-NRogi1_OOFOXcQ_vGv_4V4LA9HjLnqBvBsArLZOG-B1QldY6cFVMVQ0A9FpY3lV0Qj8tK4LOIy7MC173bWnMF9jEhF-a-K8LE_mbsJegokM9MHM8xiubwfyaAOGZqictdTvjQbF-5-FgTepYki-rSOrWvDc36rcMsgagFuxDdIXgie_NyVh8NZWw0KoJ_cTxVLv_NtH77z3ZjLw7DSzko1DiT4GZk3plkhNZJCWhYYSX-L9hRMLi6XqlZI_PwTXRZXuOX7vPuhhe_WbNVrutxTM2VER5wNAF29pJIocImPEYlsFQAcFV5ZtP4f9hiAVQIz-FFowFkMZaKw8htdjwH4nkJUNk8Z0701N-oCnCJhxbn2bkh1YQCD-eo9vF1dEvDX82ivsdNMpfDfO3DcyRRbrZB1uCmLjFvtWCfm2i-HDkXX0d6UlDD2i9VQT1yUG2PhMhrRbg_aaWKVOYWR2C1ObU1WPuckP4YA7w88wvdM_jHXMG4ehE_Nc7XTeXq5R7oyau4HVg==
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help
23 | help:
24 | @echo "Please use \`make ' where is one of"
25 | @echo " html to make standalone HTML files"
26 | @echo " dirhtml to make HTML files named index.html in directories"
27 | @echo " singlehtml to make a single large HTML file"
28 | @echo " pickle to make pickle files"
29 | @echo " json to make JSON files"
30 | @echo " htmlhelp to make HTML files and a HTML help project"
31 | @echo " qthelp to make HTML files and a qthelp project"
32 | @echo " applehelp to make an Apple Help Book"
33 | @echo " devhelp to make HTML files and a Devhelp project"
34 | @echo " epub to make an epub"
35 | @echo " epub3 to make an epub3"
36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
37 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
39 | @echo " text to make text files"
40 | @echo " man to make manual pages"
41 | @echo " texinfo to make Texinfo files"
42 | @echo " info to make Texinfo files and run them through makeinfo"
43 | @echo " gettext to make PO message catalogs"
44 | @echo " changes to make an overview of all changed/added/deprecated items"
45 | @echo " xml to make Docutils-native XML files"
46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
47 | @echo " linkcheck to check all external links for integrity"
48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
49 | @echo " coverage to run coverage check of the documentation (if enabled)"
50 | @echo " dummy to check syntax errors of document sources"
51 |
52 | .PHONY: clean
53 | clean:
54 | rm -rf $(BUILDDIR)/*
55 |
56 | .PHONY: html
57 | html: SPHINXOPTS += -W
58 | html:
59 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
60 | @echo
61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
62 |
63 | .PHONY: dirhtml
64 | dirhtml:
65 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
66 | @echo
67 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
68 |
69 | .PHONY: singlehtml
70 | singlehtml:
71 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
72 | @echo
73 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
74 |
75 | .PHONY: pickle
76 | pickle:
77 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
78 | @echo
79 | @echo "Build finished; now you can process the pickle files."
80 |
81 | .PHONY: json
82 | json:
83 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
84 | @echo
85 | @echo "Build finished; now you can process the JSON files."
86 |
87 | .PHONY: htmlhelp
88 | htmlhelp:
89 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
90 | @echo
91 | @echo "Build finished; now you can run HTML Help Workshop with the" \
92 | ".hhp project file in $(BUILDDIR)/htmlhelp."
93 |
94 | .PHONY: qthelp
95 | qthelp:
96 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
97 | @echo
98 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
99 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
100 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Travisdocsbuilder.qhcp"
101 | @echo "To view the help file:"
102 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Travisdocsbuilder.qhc"
103 |
104 | .PHONY: applehelp
105 | applehelp:
106 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
107 | @echo
108 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
109 | @echo "N.B. You won't be able to view it unless you put it in" \
110 | "~/Library/Documentation/Help or install it in your application" \
111 | "bundle."
112 |
113 | .PHONY: devhelp
114 | devhelp:
115 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
116 | @echo
117 | @echo "Build finished."
118 | @echo "To view the help file:"
119 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Travisdocsbuilder"
120 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Travisdocsbuilder"
121 | @echo "# devhelp"
122 |
123 | .PHONY: epub
124 | epub:
125 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
126 | @echo
127 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
128 |
129 | .PHONY: epub3
130 | epub3:
131 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
132 | @echo
133 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
134 |
135 | .PHONY: latex
136 | latex:
137 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
138 | @echo
139 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
140 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
141 | "(use \`make latexpdf' here to do that automatically)."
142 |
143 | .PHONY: latexpdf
144 | latexpdf:
145 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
146 | @echo "Running LaTeX files through pdflatex..."
147 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
148 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
149 |
150 | .PHONY: latexpdfja
151 | latexpdfja:
152 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
153 | @echo "Running LaTeX files through platex and dvipdfmx..."
154 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
155 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
156 |
157 | .PHONY: text
158 | text:
159 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
160 | @echo
161 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
162 |
163 | .PHONY: man
164 | man:
165 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
166 | @echo
167 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
168 |
169 | .PHONY: texinfo
170 | texinfo:
171 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
172 | @echo
173 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
174 | @echo "Run \`make' in that directory to run these through makeinfo" \
175 | "(use \`make info' here to do that automatically)."
176 |
177 | .PHONY: info
178 | info:
179 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
180 | @echo "Running Texinfo files through makeinfo..."
181 | make -C $(BUILDDIR)/texinfo info
182 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
183 |
184 | .PHONY: gettext
185 | gettext:
186 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
187 | @echo
188 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
189 |
190 | .PHONY: changes
191 | changes:
192 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
193 | @echo
194 | @echo "The overview file is in $(BUILDDIR)/changes."
195 |
196 | .PHONY: linkcheck
197 | linkcheck:
198 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
199 | @echo
200 | @echo "Link check complete; look for any errors in the above output " \
201 | "or in $(BUILDDIR)/linkcheck/output.txt."
202 |
203 | .PHONY: doctest
204 | doctest:
205 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
206 | @echo "Testing of doctests in the sources finished, look at the " \
207 | "results in $(BUILDDIR)/doctest/output.txt."
208 |
209 | .PHONY: coverage
210 | coverage:
211 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
212 | @echo "Testing of coverage in the sources finished, look at the " \
213 | "results in $(BUILDDIR)/coverage/python.txt."
214 |
215 | .PHONY: xml
216 | xml:
217 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
218 | @echo
219 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
220 |
221 | .PHONY: pseudoxml
222 | pseudoxml:
223 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
224 | @echo
225 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
226 |
227 | .PHONY: dummy
228 | dummy:
229 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
230 | @echo
231 | @echo "Build finished. Dummy builder generates no files."
232 |
--------------------------------------------------------------------------------
/docs/_templates/globaltocindex.html:
--------------------------------------------------------------------------------
1 | {#
2 | basic/globaltoc.html
3 | ~~~~~~~~~~~~~~~~~~~~
4 |
5 | Sphinx sidebar template: global table of contents.
6 |
7 | :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS.
8 | :license: BSD, see LICENSE for details.
9 |
10 | Modified to work properly with the index page
11 | #}
12 | {{ toctree() }}
13 |
--------------------------------------------------------------------------------
/docs/_templates/localtocindex.html:
--------------------------------------------------------------------------------
1 | {#
2 | basic/localtoc.html
3 | ~~~~~~~~~~~~~~~~~~~
4 |
5 | Sphinx sidebar template: local table of contents.
6 |
7 | :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS.
8 | :license: BSD, see LICENSE for details.
9 |
10 | Modified to work properly with the index page
11 | #}
12 | {%- if display_toc %}
13 |
4 | Open an issue in our issue
5 | tracker. Issues that are just questions are fine.
6 |
7 |
--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
1 | =====
2 | API
3 | =====
4 |
5 | This is the Python API. We recommend that most users use doctr from the
6 | command line.
7 |
8 | Local
9 | =====
10 |
11 | .. automodule:: doctr.local
12 | :members:
13 |
14 | Travis
15 | ======
16 |
17 | .. automodule:: doctr.travis
18 | :members:
19 |
--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
1 | =================
2 | Doctr Changelog
3 | =================
4 |
5 | 1.8.0 (2019-02-01)
6 | ==================
7 |
8 | Major Changes
9 | -------------
10 |
11 | - Doctr now supports repos hosted on travis-ci.com (in addition to .org).
12 | (:issue:`310`)
13 |
14 | Some notes about travis-ci.com support:
15 |
16 | * travis-ci.org and travis-ci.com have different public keys for the same
17 | repo, so it is necessary for ``doctr configure`` to know which is being
18 | used. If you want to move from travis-ci.org to travis-ci.com, you will
19 | need to reconfigure.
20 |
21 | * If only one of travis-ci.org or travis-ci.com is enabled, ``doctr
22 | configure`` will automatically configure the repo for that one. If both
23 | are enabled, it will ask which one to configure for. You can also use the
24 | ``--travis-tld`` command line flag to ``doctr configure`` to specify which
25 | one to use.
26 |
27 | * Once configured, there is no difference in the ``.travis.yml`` file.
28 |
29 | * If for whatever reason you want to run doctr on both, you can configure
30 | each separately, renaming the encrypted files that they generate. Add both
31 | secure environment variables to ``.travis.yml``, and do something like
32 |
33 | .. code:: bash
34 |
35 | if [[ $TRAVIS_BUILD_WEB_URL == *"travis-ci.com"* ]]; then
36 | doctr deploy --built-docs . --key-path github_deploy_key-com.enc;
37 | else
38 | doctr deploy --built-docs . --key-path github_deploy_key-org.enc;
39 | fi
40 |
41 |
42 | - New ``--no-authenticate`` flag to ``doctr configure``. This disables
43 | authentication with GitHub. If GitHub authentication is required (i.e., the
44 | repository is private), then it will fail. This
45 | flag implies ``-no-upload-key``, which now no longer disables
46 | authentication. (:issue:`310`)
47 |
48 | - Doctr does not attempt to push on Travis builds of forks. Note, this
49 | requires using the GitHub API to check if the repo is a fork, which often
50 | fails. If it does, the build will error anyway (which can be ignored). You
51 | can use `Travis conditions `_
52 | if you need a build to not fail on a fork. (:issue:`332`)
53 |
54 | - Doctr is now better tested on private repositories. Private repositories now
55 | work with both ``--token`` and a deploy key (a deploy key the default and is
56 | recommended). Note that configuring Travis for a private repository will
57 | generate a temporary personal access token on GitHub, and immediately delete
58 | it. This is necessary to authenticate with Travis. You will receive an email
59 | from GitHub about it. (:issue:`337`)
60 |
61 |
62 | Minor Changes
63 | -------------
64 |
65 | - Fix the ``--branch-whitelist`` flag with no arguments. (:issue:`330`,
66 | :issue:`334`)
67 |
68 | - Print the doctr version at the beginning of deploy. (:issue:`333`)
69 |
70 | - Fix ``doctr deploy --built-docs file`` when the deploy directory doesn't
71 | exist. (:issue:`332)`
72 |
73 | - Improved error message when doctr is not configured properly. (:issue:`338`)
74 |
75 | 1.7.4 (2018-08-19)
76 | ==================
77 |
78 | Major Changes
79 | -------------
80 |
81 | - Run a single ``git add`` and ``git rm`` command for all the files. This
82 | drastically improves the performance of doctr when there are many files that
83 | are synced. (:issue:`325`)
84 |
85 | - Improve the error messaging in ``doctr configure`` when the 2FA code
86 | expires, and when the GitHub API rate limit is hit. The GitHub API rate
87 | limit is shared across all OAuth applications, and is often hit after
88 | clicking the "sync account" button on travis-ci.org, especially if you have
89 | access to a large number of repos. If this happens, you must wait an hour
90 | and run ``doctr configure`` again. (:issue:`320`)
91 |
92 | Minor Changes
93 | -------------
94 |
95 | - Improve error messages when the deploy key isn't found. (:issue:`306`)
96 |
97 | - Doctr doesn't commit when the most recent commit on the main repo was by
98 | doctr. This avoids infinite loops if you accidentally run doctr from the
99 | ``master`` branch of a ``.github.io`` instead of a separate branch. See
100 | the :ref:`recipe-github-io` recipe. (:issue:`318`)
101 |
102 | 1.7.3 (2018-04-16)
103 | ==================
104 |
105 | Minor Changes
106 | -------------
107 |
108 | - Use the ``cryptography`` module to generate the SSH deploy key instead of
109 | ``ssh-keygen``. This makes it possible to run ``doctr configure`` on
110 | Windows. (:issue:`303`)
111 |
112 |
113 | 1.7.2 (2018-02-06)
114 | ==================
115 |
116 | Major Changes
117 | -------------
118 |
119 | - Update Travis API call to Travis API v3 (``doctr configure`` now works
120 | again). (:issue:`298`)
121 |
122 | - Add ``--exclude`` flag to ``doctr deploy`` to chose files and directories
123 | from ``--built-docs`` that should be excluded from being deployed.
124 | (:issue:`296`)
125 |
126 | Minor Changes
127 | -------------
128 |
129 | - Fix ``--built-docs .``. (:issue:`294`)
130 |
131 | 1.7.1 (2018-01-30)
132 | ==================
133 |
134 | Major Changes
135 | -------------
136 |
137 | .. role:: red
138 |
139 | .. role:: green
140 |
141 | .. role:: blue
142 |
143 | .. role:: magenta
144 |
145 | .. role:: gray
146 |
147 | .. raw:: html
148 |
149 |
151 |
152 | - Cleanup the ``doctr configure`` code. Output is now
153 | color-coded according to its meaning:
154 |
155 | - :red:`red`: warnings and errors
156 | - :green:`green`: welcome messages (used sparingly)
157 | - :blue:`blue`: default values
158 | - :magenta:`bold magenta`: action items
159 | - :gray:`bold gray`: things that should be replaced when copy-pasting
160 |
161 | The ``doctr configure`` text has also been improved. (:issue:`289`)
162 |
163 | Minor Changes
164 | -------------
165 |
166 | - Retry on invalid username and password in ``doctr configure``. (:issue:`289`)
167 |
168 | - Print when the 2FA code fails in ``doctr configure``. (:issue:`289`)
169 |
170 | - Fix the ``--branch-whitelist`` flag to ``doctr deploy``. (:issue:`291`)
171 |
172 | 1.7.0 (2017-11-21)
173 | ==================
174 |
175 | Major Changes
176 | -------------
177 |
178 | - Add support for multiple deploy repos. Thanks :user:`ylemkimon`. Note, as a
179 | result of this, the default environment variable name on Travis is now
180 | :samp:`DOCTR_DEPLOY_ENCRYPTION_KEY_{ORG}_{REPO}` where :samp:`{ORG}` and
181 | :samp:`{REPO}` are the GitHub organization and repo, capitalized with
182 | special characters replaced with underscores. The default encryption key
183 | file is now :samp:`doctr_deploy_key_{org}_{repo}.enc`, where :samp:`{org}`
184 | and :samp:`{repo}` are the organization and repo names with special
185 | characters replaced with underscores. The old key and file names are still
186 | supported for backwards compatibility, and a custom key file name can still
187 | be used with the ``--key-path`` flag. (:issue:`276` and :issue:`280`)
188 |
189 | - Add support for deploying to GitHub wikis. Thanks :user:`ylemkimon`. The
190 | wiki for a GitHub repository is :samp:`{org}/{repo}.wiki`. The deploy key
191 | for a wiki is the same as for the repository itself, so if you have already
192 | run ``doctr configure`` for a given repository you do not need to run it
193 | again for its wiki. See :ref:`the recipes page ` for more
194 | information. (:issue:`276` and :issue:`280`)
195 |
196 |
197 | - Add support for deploying from tag builds. Tag builds are builds
198 | that Travis CI runs on tags pushed up to the repository. See
199 | :ref:`the recipes page ` for more information. (:issue:`225`)
200 |
201 | Minor Changes
202 | -------------
203 |
204 | - Add a global table of contents to the docs sidebar. (:issue:`284`)
205 | - Note in the docs that doctr will make the ``gh-pages`` branch for you if it
206 | doesn't exist. Thanks :user:`CJ-Wright`. (:issue:`235`)
207 | - Print a more helpful error message when the repository check in ``doctr
208 | configure`` fails. Thanks :user:`ylemkimon`. (:issue:`279`)
209 |
210 | 1.6.3 (2017-11-11)
211 | ==================
212 |
213 | Minor Changes
214 | -------------
215 |
216 | - Fix an error that occured when ``gh-pages`` did not exist and doctr did not
217 | have the permissions to create it (e.g., on a pull request build).
218 | (:issue:`262`)
219 | - Make usernames links in the changelog. (:issue:`270`)
220 |
221 | 1.6.2 (2017-10-20)
222 | ==================
223 |
224 | Minor Changes
225 | -------------
226 |
227 | - Fix some typos in the ``doctr configure`` output. Thanks :user:`bnaul` and
228 | :user:`ocefpaf`. (:issue:`261` and :issue:`260`)
229 | - Fix the retry logic for pushing. (:issue:`265`)
230 | - Better messaging when doctr fails because of an error from a command.
231 | (:issue:`263`)
232 | - Fix an error when ``--command`` makes changes to a file that isn't synced,
233 | and no synced files are actually changed. Note, currently, if ``--command``
234 | adds or changes any files that aren't the new ones that are synced, they
235 | will not be committed unless they are manually added to the index. This
236 | should be improved in a future version (see :issue:`267`). (:issue:`266`)
237 |
238 | 1.6.1 (2017-09-27)
239 | ==================
240 |
241 | Minor Changes
242 | -------------
243 |
244 | - Revert the change to ``--command`` from 1.6.0 that makes it run on the
245 | original branch. If you want to run a command on the original branch, just
246 | run it before running doctr. ``--command`` now runs on the deploy branch, as
247 | it did before. This does not revert the other change to ``--command`` from
248 | 1.6.0 (running with ``shell=True``). (:issue:`259`)
249 |
250 | 1.6.0 (2017-09-26)
251 | ==================
252 |
253 | Major Changes
254 | -------------
255 |
256 | - Fix pushing to .github.io repos (thanks :user:`danielballan`). (:issue:`190`)
257 | - Run ``--command`` on the original branch, not the deploy branch.
258 | (:issue:`192`)
259 | - Run ``--command`` with ``shell=True``. (:issue:`193`)
260 | - Fix ``doctr configure`` for 2-factor authentication from SMS (thanks
261 | :user:`techgaun`). (:issue:`203`)
262 | - Copy ``--built-docs`` to a temporary directory before syncing. Fixes syncing
263 | of committed files. (:issue:`215`)
264 | - Only set the git username and password on Travis if they aren't set already.
265 | (:issue:`216`)
266 | - Guess the repo automatically in ``doctr configure``. (:issue:`217`)
267 | - Use ``git stash`` instead of ``git reset --hard`` on Travis. Fixes syncing
268 | tracked files with changes. (:issue:`219`)
269 | - Automatically retry on failure in Travis. Fixes race conditions from pushing
270 | from concurrent builds. (:issue:`222`)
271 | - Use the "ours" merge strategy on merge. This should avoid issues when there
272 | are merge conflicts on gh-pages from other non-doctr commits. (:issue:`232`)
273 | - Allow ``--built-docs`` to be a file. (:issue:`252`)
274 |
275 | Minor Changes
276 | -------------
277 |
278 | - Improve instructions (thanks :user:`choldgraf`). (:issue:`186`)
279 | - Skip GitHub tests if no API token is present (:issue:`187`)
280 | - Invalid input won't kill ``doctr configure`` but will instead prompt again for valid
281 | input. Prevents users from having to go through the whole login rigamarole
282 | again. (:issue:`181`, :issue:`188`)
283 | - Make it clearer in the docs that doctr isn't just for Sphinx. (:issue:`196`)
284 | - Print a red error message when doctr fails. (:issue:`239`)
285 | - Fix some rendering in the docs (thanks :user:`CJ-Wright`). (:issue:`249`)
286 | - Fix out of order command output (except when doctr uses a token). Also,
287 | print doctr commands in blue. (:issue:`250`)
288 |
289 | 1.5.3 (2017-04-07)
290 | ==================
291 | - Fix for ``doctr configure`` crashing (:issue:`179`)
292 |
293 | 1.5.2 (2017-03-29)
294 | ==================
295 | - Fix for bug that prevented deploying using ``no-require-master``
296 |
297 | 1.5.1 (2017-03-17)
298 | ==================
299 | - Fix for critical bug that allowed pushing docs from any branch. (:issue:`160`)
300 |
301 | 1.5.0 (2017-03-15)
302 | ==================
303 | - The ``--gh-pages-docs`` flag of ``doctr deploy`` has been deprecated.
304 | Specify the deploy directory like ``doctr deploy .`` or ``doctr deploy docs``.
305 | There is also no longer a default deploy directory. (:issue:`128`)
306 | - ``setup_GitHub_push`` now takes a ``branch_whitelist`` parameter instead of
307 | of a ``require_master``
308 | - ``.travis.yml`` can be used to store some of doctr configuration in addition
309 | to the command line flags. Write doctr configuration under the ``doctr`` key.
310 | (:issue:`137`)
311 | - All boolean command line flags now have a counterpart that can overwrite
312 | the config values set in ``.travis.yml``
313 | - ``doctr`` can now deploy to organization accounts (``github.io``)
314 | (:issue:`25`)
315 | - Added ``--deploy-branch-name`` flag to specify which branch docs will be
316 | deployed to
317 |
318 | 1.4.1 (2017-01-11)
319 | ==================
320 | - Fix Travis API endpoint when checking if a repo exists. (:issue:`143`)
321 | - Add warnings about needing ``set -e`` in ``.travis.yml``. (:issue:`146`)
322 | - Explicitly pull from ``doctr_remote`` on Travis. (:issue:`147`)
323 | - Don't attempt to push ``gh-pages`` to the remote when pushing is disallowed
324 | (e.g., on a pull request). (:issue:`150`)
325 | - ``doctr configure`` now deletes the public key automatically. (:issue:`151`)
326 |
327 | 1.4.0 (2016-11-11)
328 | ==================
329 |
330 | - Set the git ``user.email`` configuration option. This is now required by the
331 | latest versions of git. (:issue:`138`, :issue:`139`)
332 | - Add more information to the automated commit messages. (:issue:`134`)
333 | - Run doctr tests on Travis with a personal access token, avoiding rate
334 | limiting errors. (:issue:`133`)
335 | - Run all doctr steps except for the push on every build. Add ``--no-push``
336 | option. Thanks :user:`Carreau`. (:issue:`125`, :issue:`126`, :issue:`132`)
337 | - Clarify in docs that doctr is not just for Sphinx. (:issue:`129`,
338 | :issue:`130`)
339 | - Use the latest version of sphinxcontrib.autoprogram to build the doctr docs.
340 | (:issue:`127`)
341 | - Check that the build repo exists on Travis. (:issue:`114`, :issue:`123`)
342 |
343 | 1.3.3 (2016-09-20)
344 | ==================
345 |
346 | - Add support for private GitHub repositories using travis-ci.com (thanks
347 | :user:`dan-blanchard`). (:issue:`121`)
348 | - Add a list of projects using doctr to the docs. (:issue:`116`)
349 | - Use the sphinx-issues extension in the changelog. (:issue:`99`)
350 | - Swap "description" and "long_description" in setup.py. (:issue:`120`)
351 |
352 | 1.3.2 (2016-09-01)
353 | ==================
354 |
355 | Major Changes
356 | -------------
357 |
358 | - Fix the --built-docs option. (:issue:`111`)
359 |
360 | Minor Changes
361 | -------------
362 |
363 | - Get the setup.py description from the README. (:issue:`103`)
364 | - Add link to GitHub docs for branch protection (thanks :user:`willingc`). (:issue:`100`)
365 |
366 | 1.3.1 (2016-08-31)
367 | ==================
368 |
369 | Major Changes
370 | -------------
371 |
372 | - Fix a bug that would cause doctr to fail if run on a pull request from a
373 | fork. (:issue:`101`)
374 |
375 | 1.3 (2016-08-30)
376 | ================
377 |
378 | Major Changes
379 | -------------
380 |
381 | - Remove the ``--tmp-dir`` flag from the command line (doctr now always
382 | deploys using a log file). (:issue:`92`)
383 | - Python API: Change ``commit_docs`` to actually commit the docs (previously,
384 | it was done in ``push_docs``). (:issue:`92`)
385 | - Python API: Don't sync files or get the build dir in ``commit_docs``. This
386 | is done separately in ``__main__.py``. The Python API for ``commit_docs`` is
387 | now ``commit_docs(*, added, removed)``. (:issue:`92`)
388 | - Python API: ``sync_from_log`` automatically includes the log file in the list of added
389 | files. (:issue:`92`)
390 | - Support running doctr multiple times in the same build. (:issue:`93`, :issue:`95`)
391 | - Add ``doctr deploy --command`` to allow running a command before committing
392 | and deploying. (:issue:`97`)
393 | - Add ``doctr deploy --no-sync`` to allow disabling syncing (useful with
394 | ``doctr deploy --command``). (:issue:`97`)
395 |
396 | Minor Changes
397 | -------------
398 |
399 | - Correctly commit the log file. (:issue:`92`)
400 | - Fix sync_from_log to create dst if it doesn't exist, and add tests for this. (:issue:`92`)
401 | - Don't assume that doctr is being run from master when creating gh-pages. (:issue:`93`)
402 | - Return to the previous branch after deploying. (:issue:`93`)
403 | - Remove extra space before options in configure help text. (:issue:`90`)
404 |
405 | 1.2 (2016-08-29)
406 | ================
407 |
408 | Major Changes
409 | -------------
410 | - Allow ``--gh-pages-docs .`` (deploying to the root directory of the
411 | ``gh-pages`` branch). (:issue:`73`)
412 | - Allow deploying to a separate repo (via ``doctr deploy --deploy-repo ``). (:issue:`63`)
413 | - Automatically detect Sphinx build directory. (:issue:`6`)
414 | - Add ``--no-require-master`` flag to allow pushing from branches other than master. (:issue:`70`)
415 |
416 | Minor Changes
417 | -------------
418 | - Add a GitHub banner to the docs. (:issue:`64`)
419 | - Move to the GitHub organization `drdoctr `_. (:issue:`67`)
420 | - Check if user/org and repo are valid before generating ssh keys or pinging Travis. (:issue:`87`)
421 | - Various improvements to documentation.
422 | - Various improvements to error checking.
423 |
424 | 1.1.1 (2016-08-09)
425 | ==================
426 |
427 | Minor Changes
428 | -------------
429 |
430 | - Add installation instructions to the documentation. (:issue:`60`)
431 | - Fix some lingering "Travis docs builder" -> "Doctr", including in the git
432 | attributes on Travis. (:issue:`60`)
433 | - Better error message when the repo doesn't exist in doctr configure. (:issue:`59`)
434 | - Indicate that repo should be org/reponame in doctr configure. (:issue:`59`)
435 |
436 | 1.1 (2016-08-09)
437 | ================
438 |
439 | Major Changes
440 | -------------
441 |
442 | - Add a real command line interface with argparse. (:issue:`23`)
443 | - Split the command line into ``doctr configure`` and ``doctr deploy``. (:issue:`28`)
444 | - Add support for using GitHub deploy keys (now the default) (:issue:`30`)
445 |
446 | Minor Changes
447 | -------------
448 |
449 | - Add flags to ``doctr deploy`` to change the build and deploy locations of
450 | the docs. (:issue:`52`)
451 | - Print more helpful instructions from ``doctr configure``. (:issue:`46`)
452 | - Add more documentation. (:issue:`47`)
453 |
454 | 1.0 (2016-07-22)
455 | ================
456 |
457 | Major Changes
458 | -------------
459 |
460 | - First release. Basic support for configuring doctr to push to Travis (using
461 | a token) and deploying to gh-pages from Travis.
462 |
--------------------------------------------------------------------------------
/docs/commandline.rst:
--------------------------------------------------------------------------------
1 | =========================
2 | Doctr Command Line Help
3 | =========================
4 |
5 | .. autoprogram:: doctr.__main__:get_parser()
6 | :prog: doctr
7 |
8 |
9 | Configuration
10 | -------------
11 |
12 | In addition to command line arguments you can configure ``doctr`` using the
13 | ``.travis.yml`` files. Command line arguments take precedence over the value
14 | present in the configuration file.
15 |
16 | The configuration parameters available from the ``.travis.yml`` file mirror
17 | their command line siblings except doubledashes ``--`` and ``--no-`` prefix are
18 | dropped.
19 |
20 | Use a ``doctr`` section in your ``travis.yml`` file to store your doctr
21 | configuration:
22 |
23 | .. code:: yaml
24 |
25 | - language: python
26 | - script:
27 | - set -e
28 | - py.test
29 | - cd docs
30 | - make html
31 | - cd ..
32 | - doctr deploy .
33 | - doctr:
34 | - key-path : 'path/to/key/from/repo/root/path.key'
35 | - deploy-repo : 'myorg/myrepo'
36 |
37 |
38 | The following options are available from the configuration file and not from
39 | the command line:
40 |
41 | ``branches``:
42 | A list of regular expression that matches branches on which ``doctr`` should
43 | still deploy the documentation. For example ``.*\.x`` will deploy all
44 | branches like ``3.x``, ``4.x`` ...
45 |
46 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Doctr documentation build configuration file, created by
5 | # sphinx-quickstart on Sun Jul 17 15:34:54 2016.
6 | #
7 | # This file is execfile()d with the current directory set to its
8 | # containing dir.
9 | #
10 | # Note that not all possible configuration values are present in this
11 | # autogenerated file.
12 | #
13 | # All configuration values have a default; values that are commented out
14 | # serve to show the default.
15 |
16 | import sys
17 | import os
18 |
19 | # If extensions (or modules to document with autodoc) are in another directory,
20 | # add these directories to sys.path here. If the directory is relative to the
21 | # documentation root, use os.path.abspath to make it absolute, like shown here.
22 | sys.path.insert(0, os.path.abspath('.')) # For custom_autoprogram.py
23 | sys.path.insert(0, os.path.abspath('..')) # For doctr
24 |
25 |
26 | import doctr
27 |
28 | # -- General configuration ------------------------------------------------
29 |
30 | # If your documentation needs a minimal Sphinx version, state it here.
31 | #needs_sphinx = '1.0'
32 |
33 | # Add any Sphinx extension module names here, as strings. They can be
34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
35 | # ones.
36 | extensions = [
37 | 'sphinx.ext.autodoc',
38 | 'sphinx.ext.viewcode',
39 | 'sphinx.ext.githubpages',
40 | 'sphinxcontrib.autoprogram',
41 | 'sphinx_issues',
42 | ]
43 |
44 | issues_github_path = 'drdoctr/doctr'
45 |
46 | # Add any paths that contain templates here, relative to this directory.
47 | templates_path = ['_templates']
48 |
49 | # The suffix(es) of source filenames.
50 | # You can specify multiple suffix as a list of string:
51 | # source_suffix = ['.rst', '.md']
52 | source_suffix = '.rst'
53 |
54 | # The encoding of source files.
55 | #source_encoding = 'utf-8-sig'
56 |
57 | # The master toctree document.
58 | master_doc = 'index'
59 |
60 | # General information about the project.
61 | project = 'Doctr'
62 | copyright = '2016, Aaron Meurer and Gil Forsyth'
63 | author = 'Aaron Meurer and Gil Forsyth'
64 |
65 | # The version info for the project you're documenting, acts as replacement for
66 | # |version| and |release|, also used in various other places throughout the
67 | # built documents.
68 | #
69 | # The short X.Y version.
70 | version = doctr.__version__
71 | # The full version, including alpha/beta/rc tags.
72 | release = version
73 |
74 | # The language for content autogenerated by Sphinx. Refer to documentation
75 | # for a list of supported languages.
76 | #
77 | # This is also used if you do content translation via gettext catalogs.
78 | # Usually you set "language" from the command line for these cases.
79 | language = None
80 |
81 | # There are two options for replacing |today|: either, you set today to some
82 | # non-false value, then it is used:
83 | #today = ''
84 | # Else, today_fmt is used as the format for a strftime call.
85 | #today_fmt = '%B %d, %Y'
86 |
87 | # List of patterns, relative to source directory, that match files and
88 | # directories to ignore when looking for source files.
89 | # This patterns also effect to html_static_path and html_extra_path
90 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
91 |
92 | # The reST default role (used for this markup: `text`) to use for all
93 | # documents.
94 | #default_role = None
95 |
96 | # If true, '()' will be appended to :func: etc. cross-reference text.
97 | #add_function_parentheses = True
98 |
99 | # If true, the current module name will be prepended to all description
100 | # unit titles (such as .. function::).
101 | #add_module_names = True
102 |
103 | # If true, sectionauthor and moduleauthor directives will be shown in the
104 | # output. They are ignored by default.
105 | #show_authors = False
106 |
107 | # The name of the Pygments (syntax highlighting) style to use.
108 | pygments_style = 'sphinx'
109 |
110 | # A list of ignored prefixes for module index sorting.
111 | #modindex_common_prefix = []
112 |
113 | # If true, keep warnings as "system message" paragraphs in the built documents.
114 | #keep_warnings = False
115 |
116 | # If true, `todo` and `todoList` produce output, else they produce nothing.
117 | todo_include_todos = False
118 |
119 |
120 | # -- Options for HTML output ----------------------------------------------
121 |
122 | # The theme to use for HTML and HTML Help pages. See the documentation for
123 | # a list of builtin themes.
124 | html_theme = 'alabaster'
125 |
126 | # Theme options are theme-specific and customize the look and feel of a theme
127 | # further. For a list of options available for each theme, see the
128 | # documentation.
129 |
130 | html_theme_options = {
131 | 'github_user': 'drdoctr',
132 | 'github_repo': 'doctr',
133 | 'github_banner': True,
134 | 'logo_name': True,
135 | 'travis_button': True,
136 | 'show_related': True,
137 | }
138 |
139 | html_sidebars = {
140 | '**': ['globaltoc.html', 'sidebarhelp.html',
141 | 'searchbox.html'],
142 | 'index': ['localtocindex.html', 'globaltocindex.html', 'sidebarhelp.html',
143 | 'searchbox.html'],
144 | }
145 |
146 | # Add any paths that contain custom themes here, relative to this directory.
147 | #html_theme_path = []
148 |
149 | # The name for this set of Sphinx documents.
150 | # " v documentation" by default.
151 | #html_title = 'Doctr v1.0'
152 |
153 | # A shorter title for the navigation bar. Default is the same as html_title.
154 | #html_short_title = None
155 |
156 | # The name of an image file (relative to this directory) to place at the top
157 | # of the sidebar.
158 | #html_logo = None
159 |
160 | # The name of an image file (relative to this directory) to use as a favicon of
161 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
162 | # pixels large.
163 | #html_favicon = None
164 |
165 | # Add any paths that contain custom static files (such as style sheets) here,
166 | # relative to this directory. They are copied after the builtin static files,
167 | # so a file named "default.css" will overwrite the builtin "default.css".
168 | # html_static_path = ['_static']
169 |
170 | # Add any extra paths that contain custom files (such as robots.txt or
171 | # .htaccess) here, relative to this directory. These files are copied
172 | # directly to the root of the documentation.
173 | #html_extra_path = []
174 |
175 | # If not None, a 'Last updated on:' timestamp is inserted at every page
176 | # bottom, using the given strftime format.
177 | # The empty string is equivalent to '%b %d, %Y'.
178 | #html_last_updated_fmt = None
179 |
180 | # If true, SmartyPants will be used to convert quotes and dashes to
181 | # typographically correct entities.
182 | #html_use_smartypants = True
183 |
184 | # Custom sidebar templates, maps document names to template names.
185 | #html_sidebars = {}
186 |
187 | # Additional templates that should be rendered to pages, maps page names to
188 | # template names.
189 | #html_additional_pages = {}
190 |
191 | # If false, no module index is generated.
192 | #html_domain_indices = True
193 |
194 | # If false, no index is generated.
195 | #html_use_index = True
196 |
197 | # If true, the index is split into individual pages for each letter.
198 | #html_split_index = False
199 |
200 | # If true, links to the reST sources are added to the pages.
201 | #html_show_sourcelink = True
202 |
203 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
204 | #html_show_sphinx = True
205 |
206 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
207 | #html_show_copyright = True
208 |
209 | # If true, an OpenSearch description file will be output, and all pages will
210 | # contain a tag referring to it. The value of this option must be the
211 | # base URL from which the finished HTML is served.
212 | #html_use_opensearch = ''
213 |
214 | # This is the file name suffix for HTML files (e.g. ".xhtml").
215 | #html_file_suffix = None
216 |
217 | # Language to be used for generating the HTML full-text search index.
218 | # Sphinx supports the following languages:
219 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
220 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
221 | #html_search_language = 'en'
222 |
223 | # A dictionary with options for the search language support, empty by default.
224 | # 'ja' uses this config value.
225 | # 'zh' user can custom change `jieba` dictionary path.
226 | #html_search_options = {'type': 'default'}
227 |
228 | # The name of a javascript file (relative to the configuration directory) that
229 | # implements a search results scorer. If empty, the default will be used.
230 | #html_search_scorer = 'scorer.js'
231 |
232 | # Output file base name for HTML help builder.
233 | htmlhelp_basename = 'doctrdoc'
234 |
235 | # -- Options for LaTeX output ---------------------------------------------
236 |
237 | latex_elements = {
238 | # The paper size ('letterpaper' or 'a4paper').
239 | #'papersize': 'letterpaper',
240 |
241 | # The font size ('10pt', '11pt' or '12pt').
242 | #'pointsize': '10pt',
243 |
244 | # Additional stuff for the LaTeX preamble.
245 | #'preamble': '',
246 |
247 | # Latex figure (float) alignment
248 | #'figure_align': 'htbp',
249 | }
250 |
251 | # Grouping the document tree into LaTeX files. List of tuples
252 | # (source start file, target name, title,
253 | # author, documentclass [howto, manual, or own class]).
254 | latex_documents = [
255 | (master_doc, 'doctr.tex', 'Doctr Documentation',
256 | 'Aaron Meurer and Gil Forsyth', 'manual'),
257 | ]
258 |
259 | # The name of an image file (relative to this directory) to place at the top of
260 | # the title page.
261 | #latex_logo = None
262 |
263 | # For "manual" documents, if this is true, then toplevel headings are parts,
264 | # not chapters.
265 | #latex_use_parts = False
266 |
267 | # If true, show page references after internal links.
268 | #latex_show_pagerefs = False
269 |
270 | # If true, show URL addresses after external links.
271 | #latex_show_urls = False
272 |
273 | # Documents to append as an appendix to all manuals.
274 | #latex_appendices = []
275 |
276 | # If false, no module index is generated.
277 | #latex_domain_indices = True
278 |
279 |
280 | # -- Options for manual page output ---------------------------------------
281 |
282 | # One entry per manual page. List of tuples
283 | # (source start file, name, description, authors, manual section).
284 | man_pages = [
285 | (master_doc, 'doctr', 'Doctr Documentation',
286 | [author], 1)
287 | ]
288 |
289 | # If true, show URL addresses after external links.
290 | #man_show_urls = False
291 |
292 |
293 | # -- Options for Texinfo output -------------------------------------------
294 |
295 | # Grouping the document tree into Texinfo files. List of tuples
296 | # (source start file, target name, title, author,
297 | # dir menu entry, description, category)
298 | texinfo_documents = [
299 | (master_doc, 'Doctr', 'Doctr Documentation',
300 | author, 'Doctr', 'One line description of project.',
301 | 'Miscellaneous'),
302 | ]
303 |
304 | # Documents to append as an appendix to all manuals.
305 | #texinfo_appendices = []
306 |
307 | # If false, no module index is generated.
308 | #texinfo_domain_indices = True
309 |
310 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
311 | #texinfo_show_urls = 'footnote'
312 |
313 | # If true, do not generate a @detailmenu in the "Top" node's menu.
314 | #texinfo_no_detailmenu = False
315 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. Doctr documentation master file, created by
2 | sphinx-quickstart on Sun Jul 17 15:34:54 2016.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 |
7 | .. include:: ../README.rst
8 |
9 | Contents:
10 |
11 | .. toctree::
12 | :maxdepth: 2
13 |
14 | self
15 | commandline
16 | recipes
17 | api
18 | changelog
19 | releasing
20 | tests
21 |
22 | Indices and tables
23 | ==================
24 |
25 | * :ref:`genindex`
26 | * :ref:`modindex`
27 | * :ref:`search`
28 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | if "%SPHINXBUILD%" == "" (
6 | set SPHINXBUILD=sphinx-build
7 | )
8 | set BUILDDIR=_build
9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
10 | set I18NSPHINXOPTS=%SPHINXOPTS% .
11 | if NOT "%PAPER%" == "" (
12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
14 | )
15 |
16 | if "%1" == "" goto help
17 |
18 | if "%1" == "help" (
19 | :help
20 | echo.Please use `make ^` where ^ is one of
21 | echo. html to make standalone HTML files
22 | echo. dirhtml to make HTML files named index.html in directories
23 | echo. singlehtml to make a single large HTML file
24 | echo. pickle to make pickle files
25 | echo. json to make JSON files
26 | echo. htmlhelp to make HTML files and a HTML help project
27 | echo. qthelp to make HTML files and a qthelp project
28 | echo. devhelp to make HTML files and a Devhelp project
29 | echo. epub to make an epub
30 | echo. epub3 to make an epub3
31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
32 | echo. text to make text files
33 | echo. man to make manual pages
34 | echo. texinfo to make Texinfo files
35 | echo. gettext to make PO message catalogs
36 | echo. changes to make an overview over all changed/added/deprecated items
37 | echo. xml to make Docutils-native XML files
38 | echo. pseudoxml to make pseudoxml-XML files for display purposes
39 | echo. linkcheck to check all external links for integrity
40 | echo. doctest to run all doctests embedded in the documentation if enabled
41 | echo. coverage to run coverage check of the documentation if enabled
42 | echo. dummy to check syntax errors of document sources
43 | goto end
44 | )
45 |
46 | if "%1" == "clean" (
47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
48 | del /q /s %BUILDDIR%\*
49 | goto end
50 | )
51 |
52 |
53 | REM Check if sphinx-build is available and fallback to Python version if any
54 | %SPHINXBUILD% 1>NUL 2>NUL
55 | if errorlevel 9009 goto sphinx_python
56 | goto sphinx_ok
57 |
58 | :sphinx_python
59 |
60 | set SPHINXBUILD=python -m sphinx.__init__
61 | %SPHINXBUILD% 2> nul
62 | if errorlevel 9009 (
63 | echo.
64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
65 | echo.installed, then set the SPHINXBUILD environment variable to point
66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
67 | echo.may add the Sphinx directory to PATH.
68 | echo.
69 | echo.If you don't have Sphinx installed, grab it from
70 | echo.http://sphinx-doc.org/
71 | exit /b 1
72 | )
73 |
74 | :sphinx_ok
75 |
76 |
77 | if "%1" == "html" (
78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
79 | if errorlevel 1 exit /b 1
80 | echo.
81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
82 | goto end
83 | )
84 |
85 | if "%1" == "dirhtml" (
86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
87 | if errorlevel 1 exit /b 1
88 | echo.
89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
90 | goto end
91 | )
92 |
93 | if "%1" == "singlehtml" (
94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
95 | if errorlevel 1 exit /b 1
96 | echo.
97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
98 | goto end
99 | )
100 |
101 | if "%1" == "pickle" (
102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
103 | if errorlevel 1 exit /b 1
104 | echo.
105 | echo.Build finished; now you can process the pickle files.
106 | goto end
107 | )
108 |
109 | if "%1" == "json" (
110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
111 | if errorlevel 1 exit /b 1
112 | echo.
113 | echo.Build finished; now you can process the JSON files.
114 | goto end
115 | )
116 |
117 | if "%1" == "htmlhelp" (
118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
119 | if errorlevel 1 exit /b 1
120 | echo.
121 | echo.Build finished; now you can run HTML Help Workshop with the ^
122 | .hhp project file in %BUILDDIR%/htmlhelp.
123 | goto end
124 | )
125 |
126 | if "%1" == "qthelp" (
127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
128 | if errorlevel 1 exit /b 1
129 | echo.
130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
131 | .qhcp project file in %BUILDDIR%/qthelp, like this:
132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Travisdocsbuilder.qhcp
133 | echo.To view the help file:
134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Travisdocsbuilder.ghc
135 | goto end
136 | )
137 |
138 | if "%1" == "devhelp" (
139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
140 | if errorlevel 1 exit /b 1
141 | echo.
142 | echo.Build finished.
143 | goto end
144 | )
145 |
146 | if "%1" == "epub" (
147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
148 | if errorlevel 1 exit /b 1
149 | echo.
150 | echo.Build finished. The epub file is in %BUILDDIR%/epub.
151 | goto end
152 | )
153 |
154 | if "%1" == "epub3" (
155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3
156 | if errorlevel 1 exit /b 1
157 | echo.
158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3.
159 | goto end
160 | )
161 |
162 | if "%1" == "latex" (
163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
164 | if errorlevel 1 exit /b 1
165 | echo.
166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
167 | goto end
168 | )
169 |
170 | if "%1" == "latexpdf" (
171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
172 | cd %BUILDDIR%/latex
173 | make all-pdf
174 | cd %~dp0
175 | echo.
176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
177 | goto end
178 | )
179 |
180 | if "%1" == "latexpdfja" (
181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
182 | cd %BUILDDIR%/latex
183 | make all-pdf-ja
184 | cd %~dp0
185 | echo.
186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
187 | goto end
188 | )
189 |
190 | if "%1" == "text" (
191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
192 | if errorlevel 1 exit /b 1
193 | echo.
194 | echo.Build finished. The text files are in %BUILDDIR%/text.
195 | goto end
196 | )
197 |
198 | if "%1" == "man" (
199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
200 | if errorlevel 1 exit /b 1
201 | echo.
202 | echo.Build finished. The manual pages are in %BUILDDIR%/man.
203 | goto end
204 | )
205 |
206 | if "%1" == "texinfo" (
207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
208 | if errorlevel 1 exit /b 1
209 | echo.
210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
211 | goto end
212 | )
213 |
214 | if "%1" == "gettext" (
215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
216 | if errorlevel 1 exit /b 1
217 | echo.
218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
219 | goto end
220 | )
221 |
222 | if "%1" == "changes" (
223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
224 | if errorlevel 1 exit /b 1
225 | echo.
226 | echo.The overview file is in %BUILDDIR%/changes.
227 | goto end
228 | )
229 |
230 | if "%1" == "linkcheck" (
231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
232 | if errorlevel 1 exit /b 1
233 | echo.
234 | echo.Link check complete; look for any errors in the above output ^
235 | or in %BUILDDIR%/linkcheck/output.txt.
236 | goto end
237 | )
238 |
239 | if "%1" == "doctest" (
240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
241 | if errorlevel 1 exit /b 1
242 | echo.
243 | echo.Testing of doctests in the sources finished, look at the ^
244 | results in %BUILDDIR%/doctest/output.txt.
245 | goto end
246 | )
247 |
248 | if "%1" == "coverage" (
249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
250 | if errorlevel 1 exit /b 1
251 | echo.
252 | echo.Testing of coverage in the sources finished, look at the ^
253 | results in %BUILDDIR%/coverage/python.txt.
254 | goto end
255 | )
256 |
257 | if "%1" == "xml" (
258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
259 | if errorlevel 1 exit /b 1
260 | echo.
261 | echo.Build finished. The XML files are in %BUILDDIR%/xml.
262 | goto end
263 | )
264 |
265 | if "%1" == "pseudoxml" (
266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
267 | if errorlevel 1 exit /b 1
268 | echo.
269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
270 | goto end
271 | )
272 |
273 | if "%1" == "dummy" (
274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy
275 | if errorlevel 1 exit /b 1
276 | echo.
277 | echo.Build finished. Dummy builder generates no files.
278 | goto end
279 | )
280 |
281 | :end
282 |
--------------------------------------------------------------------------------
/docs/recipes.rst:
--------------------------------------------------------------------------------
1 | .. _recipes:
2 |
3 | =========
4 | Recipes
5 | =========
6 |
7 | Here are some useful recipes for Doctr.
8 |
9 | .. _any-branch:
10 |
11 | Deploy docs from any branch
12 | ===========================
13 |
14 | .. role:: raw-html(raw)
15 | :format: html
16 |
17 | By default, Doctr only deploys docs from the ``master`` branch, but it can be
18 | useful to deploy docs from other branches, to test them out.
19 |
20 | The branch name on Travis is stored in the ``$TRAVIS_BRANCH`` environment
21 | variable. One suggestion would be to deploy the docs to a special directory
22 | for each branch. The following will deploy the docs to ``docs`` on master and
23 | :raw-html:`docs-branch` on *branch*.
24 |
25 | .. code:: yaml
26 |
27 | - if [[ "${TRAVIS_BRANCH}" == "master" ]]; then
28 | doctr deploy docs;
29 | else
30 | doctr deploy --no-require-master "docs-$TRAVIS_BRANCH";
31 | fi
32 |
33 | This will not remove the docs after the branch is merged. You will need to do
34 | that manually.
35 |
36 | .. TODO: How can we add steps to do that automatically?
37 |
38 | **Note**: It is only possible to deploy docs from branches on the same repo.
39 | For security purposes, it is not possible to deploy from branches on forks
40 | (Travis does not allow access to encrypted environment variables on pull
41 | requests from forks). If you want to deploy the docs for a branch from a pull
42 | request, you will need to push it up to the main repository.
43 |
44 | .. _non-master-branch:
45 |
46 | Deploy docs from a non-master branch
47 | ====================================
48 |
49 | If you want to deploy docs from only specific branches other than just
50 | ``master``, you can use the ``--branch-whitelist`` flag. This is useful if
51 | your default branch is named something other than ``master``. The default
52 | ``--branch-whitelist`` is ``master``. ``--branch-whitelist`` can take any
53 | number of arguments, so it should generally go last in your ``doctr deploy``
54 | call.
55 |
56 | .. code:: yaml
57 |
58 | - doctr deploy --built-docs build/ . --branch-whitelist develop
59 |
60 | .. _recipe-tags:
61 |
62 | Deploy docs from git tags
63 | =========================
64 |
65 | Travis CI runs separate builds for git tags that are pushed to your repo. By
66 | default, doctr does not deploy on these builds, but it can be enabled with the
67 | ``--build-tags`` flag to ``doctr deploy``. This is useful if you want to use
68 | doctr to deploy versioned docs for releases, for example.
69 |
70 | On Travis CI, the tag is set to the environment variable ``$TRAVIS_TAG``,
71 | which is empty otherwise. The following will deploy the docs to ``dev`` for
72 | normal ``master`` builds, and ``version-`` for tag builds:
73 |
74 | .. code:: yaml
75 |
76 | - if [[ -z "$TRAVIS_TAG" ]]; then
77 | DEPLOY_DIR=dev;
78 | else
79 | DEPLOY_DIR="version-$TRAVIS_TAG";
80 | fi
81 | - doctr deploy --build-tags --built-docs build/ $DEPLOY_DIR
82 |
83 | If you want to deploy only on a tag, use ``--branch-whitelist`` with no
84 | arguments to tell doctr to not deploy from any branch. For instance, to deploy
85 | only tags to ``latest``:
86 |
87 | .. code:: yaml
88 |
89 | - doctr deploy latest --built-docs build/ --build-tags --branch-whitelist
90 |
91 | Deploy to a separate repo
92 | =========================
93 |
94 | By default, Doctr deploys to the ``gh-pages`` branch of the same repository it
95 | is run from, but you can deploy to the ``gh-pages`` branch of any repository.
96 |
97 | To do this, specify a separate deploy and build repository when running
98 | ``doctr configure`` (it will ask you for the two separately). You will need
99 | admin access to the deploy repository to upload the deploy key (``doctr
100 | configure`` will prompt you for your GitHub credentials). If you do not have
101 | access, you can run ``doctr configure --no-upload-key``. This will print out the
102 | public deploy key, which you can then give to someone who has admin access to
103 | add to the form on GitHub (``doctr configure`` will print the public key and
104 | the url of the form for someone with admin access to paste it in).
105 |
106 | In your ``.travis.yml``, specify the deploy repository with
107 |
108 | .. code:: yaml
109 |
110 | - doctr deploy --deploy-repo deploy_dir
111 |
112 | The instructions from ``doctr configure`` will also give you the correct
113 | command to run.
114 |
115 | Setting up Doctr for a repo you don't have admin access to
116 | ==========================================================
117 |
118 | ``doctr configure`` by default asks for your GitHub credentials so that it can
119 | upload the deploy key it creates. However, if you do not have admin access to
120 | the repository you are deploying to, you cannot upload the deploy key.
121 |
122 | No worries, you can still help. Run
123 |
124 | .. code:: bash
125 |
126 | doctr configure --no-upload-key
127 |
128 | This will set up doctr, but not require any GitHub credentials. Follow the
129 | instructions on screen. Create a new branch, commit the
130 | ``github_deploy_key_org_repo.enc`` file, and edit ``.travis.yml`` to include the
131 | encrypted environment variable and the call to ``doctr deploy``.
132 |
133 | Then, create a pull request to the repository. Tell the owner of the
134 | repository to add the public key which Doctr has printed as a deploy key for
135 | the repo (Doctr will also print the url where they can add this). Don't worry,
136 | the key is a public SSH key, so it's OK to post it publicly in the pull
137 | request.
138 |
139 | Post-processing the docs on gh-pages
140 | ====================================
141 |
142 | Sometimes you may want to post-process your docs on the ``gh-pages`` branch.
143 | For example, you may want to add some links to other versions in your
144 | index.html.
145 |
146 | You can run any command on the ``gh-pages`` branch with the ``doctr deploy
147 | --command`` flag. This is run after the docs are synced to ``gh-pages`` but
148 | before they are committed and uploaded.
149 |
150 | For example, if you have a script in ``gh-pages`` called ``post-process.py``,
151 | you can run
152 |
153 | .. code:: bash
154 |
155 | doctr deploy --command 'post-process.py' deploy_dir
156 |
157 | Using a separate command to deploy to gh-pages
158 | ==============================================
159 |
160 | If you already have an existing tool to deploy to ``gh-pages``, you can still
161 | use Doctr to manage your deploy key. Use
162 |
163 | .. code:: bash
164 |
165 | doctr deploy --no-sync --command 'command to deploy' deploy_dir
166 |
167 | The command to deploy should add any files that you want committed to the
168 | index.
169 |
170 | .. _recipe-wikis:
171 |
172 | Deploying to a GitHub wiki
173 | ==========================
174 |
175 | Doctr supports deploying to GitHub wikis. Just use ``org/repo.wiki`` when
176 | as the deploy repo running ``doctr configure``. When deploying, use
177 |
178 | .. code:: bash
179 |
180 | doctr deploy --deploy-repo org/repo.wiki .
181 |
182 | The deploy key for pushing to a wiki is the same as for pushing to the repo
183 | itself, so if you are pushing to both, you will not need more than one deploy
184 | key.
185 |
186 | .. _recipe-github-io:
187 |
188 | Using doctr with ``*.github.io`` pages
189 | ======================================
190 |
191 | Github allows users to create pages at the root URL of users' or
192 | organizations' http://github.io pages. For example, an organization
193 | ``coolteam`` can setup a repository at
194 | ``https://github.com/coolteam/coolteam.github.io`` and the html files in the
195 | ``master`` branch of this repository will be served to
196 | ``https://coolteam.github.io``.
197 |
198 | With doctr, it is necessary to separate the website source files, e.g. input to
199 | a static site generator, from the output HTML files into two different
200 | branches. The output files must be stored in the ``master`` branch, as per
201 | Github's specification. The source files can be stored in another custom branch
202 | of your choosing, below the name ``source`` is chosen.
203 |
204 | To do this:
205 |
206 | 1. Create a new branch for the source files, e.g. named ``source``, and push
207 | this to Github.
208 | 2. Set this branch as the default branch in the Github settings for the
209 | repository.
210 | 3. Run the ``doctr configure`` command in the ``source`` branch. The source and
211 | output repositories should both be set to ``coolteam/coolteam.github.io`` in
212 | the configuration options.
213 | 4. Commit the generated encryption key and the ``.travis.yml`` file to the
214 | ``source`` branch. Do not commit a ``.travis.yml`` file to both the
215 | ``master`` and ``source`` branches, as this will also cause and infinite
216 | loop of Travis builds.
217 | 5. Lastly, in ``.travis.yml`` make sure that the ``doctr deploy`` command white
218 | lists the ``source`` branch, like so:
219 |
220 | .. code:: bash
221 |
222 | doctr deploy --branch-whitelist source --built-docs output-directory/ .
223 |
224 | The source files should only be pushed to the ``source`` branch and all output
225 | files will be pushed to the ``master`` branch during the Travis builds.
226 |
--------------------------------------------------------------------------------
/docs/releasing.rst:
--------------------------------------------------------------------------------
1 | Releasing
2 | ---------
3 |
4 | Here is how to do a release:
5 |
6 | - Create a release branch (branch protection makes it impossible to push
7 | directly to master, so you have to release from a branch). I recommend
8 | naming the branch something other than the release number, as that makes the
9 | below commands not work until you delete the branch.
10 | - Update ``docs/changelog.rst``. Add the release date.
11 | - Make a pull request with the release branch.
12 | - Make sure all the Travis checks pass on the commit you plan to tag.
13 | - Tag the release. The tag name should be the version number of the release,
14 | like ``git tag 2.0 -a``. Include the ``-a`` flag. This will ask for some
15 | commit message for the tag (you can just use something like "Doctr 2.0
16 | release", or you can put the changelog in there if you want).
17 | - Do ``python setup.py sdist bdist_wheel upload``. It uses the tag to get the version number, so
18 | you need to do this after you tag.
19 | - Push up the tag (``git push origin 2.0``).
20 | - Merge the pull request.
21 | - Create a pull request to the `conda-forge feedstock
22 | `_ to update it. Make sure
23 | to do a pull request from a fork. Merge it once those tests pass.
24 |
--------------------------------------------------------------------------------
/docs/tests.rst:
--------------------------------------------------------------------------------
1 | =========================
2 | Notes for testing doctr
3 | =========================
4 |
5 | GitHub Test Authentication
6 | --------------------------
7 |
8 | Many of the ``doctr`` tests in ``doctr/tests/test_local.py`` contact GitHub to
9 | check that invalid repo names raise errors, etc. However, the GitHub API has a
10 | hard limit of 60 requests / hour for unauthenticated requests and it is very
11 | easy to run over this when pushing many changes.
12 |
13 | In order to avoid this limit, there is a GitHub Personal Access Token stored in
14 | the ``doctr`` Travis account that is available as the environment variable
15 | ``$TESTING_TOKEN``.
16 |
17 | To regenerate / change this token, first go to `GitHub Settings
18 | `_ and create a Personal Access Token. Make
19 | sure that all of the checkboxes are unchecked (this token should only have
20 | privileges to check in with the GitHub API).
21 |
22 | You can add the updated token to Travis on the `doctr Travis Settings Page
23 | `_.
24 |
25 | Paste the token string into the ``Value`` field and ``TESTING_TOKEN`` in the
26 | ``Name`` field (unless you have changed this value in
27 | ``doctr/tests/test_local.py``).
28 |
29 | travis-ci.com cs. travis-ci.org
30 | -------------------------------
31 |
32 | Travis CI is `migrating
33 | `_
34 | from .org to .com. While the migration is in place, it is possible to enable a
35 | repository on both. However, the same repository on each will have different
36 | public keys. Doctr presently only supports one at a time. On Travis itself,
37 | there is little difference in the doctr code, but there is a lot of code in
38 | ``configure`` to automatically determine which is enabled.
39 |
40 | The repos:
41 |
42 | - https://github.com/drdoctr/testing-travis-ci-com
43 | - https://github.com/drdoctr/testing-travis-ci-org
44 | - https://github.com/drdoctr/testing-travis-ci-both
45 | - https://github.com/drdoctr/testing-travis-ci-neither
46 |
47 | Are enabled on Travis CI .com, .org, both, and neither. To enable a repo on
48 | .org, go to https://travis-ci.org/organizations/drdoctr/repositories and make
49 | sure it is checked. To enable a repo on .com, go to the `Travis CI Apps
50 | settings
51 | `_ on
52 | GitHub, and make sure "only selected repositories" is enabled with those repos
53 | that should be enabled.
54 |
55 | There are automated tests in the test suite that check the function that
56 | determines which of .org/.com it is enabled on, which test against these repos
57 | (``test_check_repo_exists_org_com``).
58 |
59 | Private Repositories
60 | --------------------
61 |
62 | Doctr also supports private repositories on GitHub. GitHub allows free private
63 | repositories, but they must be made on a user account, not the ``drdoctr``
64 | org.
65 |
66 | To build a private repo, you have to use travis-ci.com. The free plan only
67 | allows 100 builds, so you might have to make a new user to continue testing.
68 | Unless we get a paid plan, testing should only be done manually, when
69 | necessary.
70 |
71 | GitHub does not allow GitHub pages on private repositories on the free plan,
72 | but you can just manually verify that things are pushed to the ``gh-pages``
73 | branch.
74 |
--------------------------------------------------------------------------------
/doctr/__init__.py:
--------------------------------------------------------------------------------
1 | from .local import (encrypt_variable, encrypt_to_file, GitHub_post,
2 | generate_GitHub_token, upload_GitHub_deploy_key, generate_ssh_key,
3 | check_repo_exists, guess_github_repo)
4 | from .travis import (decrypt_file, setup_deploy_key, get_token, run,
5 | setup_GitHub_push, checkout_deploy_branch, deploy_branch_exists,
6 | set_git_user_email, create_deploy_branch, copy_to_tmp, sync_from_log,
7 | commit_docs, push_docs, get_current_repo, find_sphinx_build_dir)
8 |
9 | __all__ = [
10 | 'encrypt_variable', 'encrypt_to_file', 'GitHub_post',
11 | 'generate_GitHub_token', 'upload_GitHub_deploy_key', 'generate_ssh_key',
12 | 'check_repo_exists', 'guess_github_repo',
13 |
14 | 'decrypt_file', 'setup_deploy_key', 'get_token', 'run',
15 | 'setup_GitHub_push', 'set_git_user_email', 'checkout_deploy_branch', 'deploy_branch_exists',
16 | 'create_deploy_branch', 'copy_to_tmp', 'sync_from_log', 'commit_docs', 'push_docs', 'get_current_repo', 'find_sphinx_build_dir'
17 | ]
18 |
19 | from ._version import get_versions
20 | __version__ = get_versions()['version']
21 | del get_versions
22 |
--------------------------------------------------------------------------------
/doctr/__main__.py:
--------------------------------------------------------------------------------
1 | """
2 | doctr
3 |
4 | A tool to automatically deploy docs to GitHub pages from Travis CI.
5 |
6 | The doctr command is two commands in one. To use, first run::
7 |
8 | doctr configure
9 |
10 | on your local machine. This will prompt for your GitHub credentials and the
11 | name of the repo you want to deploy docs for. This will generate a secure key,
12 | which you should insert into your .travis.yml.
13 |
14 | Then, on Travis, for the build where you build your docs, add::
15 |
16 | - doctr deploy . --built-docs path/to/built/html/
17 |
18 | to the end of the build to deploy the docs to GitHub pages. This will only
19 | run on the master branch, and won't run on pull requests.
20 |
21 | For more information, see https://drdoctr.github.io/doctr/docs/
22 | """
23 |
24 | import sys
25 | import os
26 | import os.path
27 | import argparse
28 | import subprocess
29 | import yaml
30 | import json
31 | import shlex
32 |
33 | from pathlib import Path
34 |
35 | from textwrap import dedent
36 |
37 | from .local import (generate_GitHub_token, encrypt_variable, encrypt_to_file,
38 | upload_GitHub_deploy_key, generate_ssh_key, check_repo_exists,
39 | GitHub_login, guess_github_repo, AuthenticationFailed, GitHubError,
40 | get_travis_token)
41 | from .travis import (setup_GitHub_push, commit_docs, push_docs,
42 | get_current_repo, sync_from_log, find_sphinx_build_dir, run,
43 | get_travis_branch, copy_to_tmp, checkout_deploy_branch)
44 |
45 | from .common import (red, green, blue, bold_black, bold_magenta, BOLD_BLACK,
46 | BOLD_MAGENTA, RESET, input)
47 |
48 | from . import __version__
49 |
50 | # See https://github.com/organizations/drdoctr/settings/applications/1418010
51 | DOCTR_CLIENT_ID = "dcd97ff81716d4498a7d"
52 |
53 | def make_parser_with_config_adder(parser, config):
54 | """factory function for a smarter parser:
55 |
56 | return an utility function that pull default from the config as well.
57 |
58 | Pull the default for parser not only from the ``default`` kwarg,
59 | but also if an identical value is find in ``config`` where leading
60 | ``--`` or ``--no`` is removed.
61 |
62 | If the option is a boolean flag, automatically register an opposite,
63 | exclusive option by prepending or removing the `--no-`. This is useful
64 | to overwrite config in ``.travis.yml``
65 |
66 | Mutate the config object and remove know keys in order to detect unused
67 | options afterwoard.
68 | """
69 |
70 | def internal(arg, **kwargs):
71 | invert = {
72 | 'store_true':'store_false',
73 | 'store_false':'store_true',
74 | }
75 | if arg.startswith('--no-'):
76 | key = arg[5:]
77 | else:
78 | key = arg[2:]
79 | if 'default' in kwargs:
80 | if key in config:
81 | kwargs['default'] = config[key]
82 | del config[key]
83 | action = kwargs.get('action')
84 | if action in invert:
85 | exclusive_grp = parser.add_mutually_exclusive_group()
86 | exclusive_grp.add_argument(arg, **kwargs)
87 | kwargs['action'] = invert[action]
88 | kwargs['help'] = 'Inverse of "%s"' % arg
89 | if arg.startswith('--no-'):
90 | arg = '--%s' % arg[5:]
91 | else:
92 | arg = '--no-%s' % arg[2:]
93 | exclusive_grp.add_argument(arg, **kwargs)
94 | else:
95 | parser.add_argument(arg, **kwargs)
96 |
97 | return internal
98 |
99 |
100 | def get_parser(config=None):
101 | """
102 | return a parser suitable to parse CL arguments.
103 |
104 | Parameters
105 | ----------
106 |
107 | config: dict
108 | Default values to fall back on, if not given.
109 |
110 | Returns
111 | -------
112 |
113 | An argparse parser configured to parse the command lines arguments of
114 | sys.argv which will default on values present in ``config``.
115 | """
116 | # This uses RawTextHelpFormatter so that the description (the docstring of
117 | # this module) is formatted correctly. Unfortunately, that means that
118 | # parser help is not text wrapped (but all other help is).
119 | parser = argparse.ArgumentParser(description=__doc__,
120 | formatter_class=argparse.RawTextHelpFormatter, epilog="""
121 | Run --help on the subcommands like 'doctr deploy --help' to see the
122 | options available.
123 | """,
124 | )
125 |
126 | if not config:
127 | config={}
128 | parser.add_argument('-V', '--version', action='version', version='doctr ' + __version__)
129 |
130 | subcommand = parser.add_subparsers(title='subcommand', dest='subcommand')
131 |
132 | deploy_parser = subcommand.add_parser('deploy', help="""Deploy the docs to GitHub from Travis.""")
133 | deploy_parser.set_defaults(func=deploy)
134 | deploy_parser_add_argument = make_parser_with_config_adder(deploy_parser, config)
135 | deploy_parser_add_argument('--force', action='store_true', help="""Run the deploy command even
136 | if we do not appear to be on Travis.""")
137 | deploy_parser_add_argument('deploy_directory', type=str, nargs='?',
138 | help="""Directory to deploy the html documentation to on gh-pages.""")
139 | deploy_parser_add_argument('--token', action='store_true', default=False,
140 | help="""Push to GitHub using a personal access token. Use this if you
141 | used 'doctr configure --token'.""")
142 | deploy_parser_add_argument('--key-path', default=None,
143 | help="""Path of the encrypted GitHub deploy key. The default is github_deploy_key_+
144 | deploy respository name + .enc.""")
145 | deploy_parser_add_argument('--built-docs', default=None,
146 | help="""Location of the built html documentation to be deployed to gh-pages. If not
147 | specified, Doctr will try to automatically detect build location
148 | (right now only works for Sphinx docs).""")
149 | deploy_parser.add_argument('--deploy-branch-name', default=None,
150 | help="""Name of the branch to deploy to (default: 'master' for ``*.github.io``
151 | and wiki repos, 'gh-pages' otherwise)""")
152 | deploy_parser_add_argument('--tmp-dir', default=None,
153 | help=argparse.SUPPRESS)
154 | deploy_parser_add_argument('--deploy-repo', default=None, help="""Repo to
155 | deploy the docs to. By default, it deploys to the repo Doctr is run from.""")
156 | deploy_parser_add_argument('--branch-whitelist', default=None, nargs='*',
157 | help="""Branches to deploy from. Pass no arguments to not build on any branch
158 | (typically used in conjunction with --build-tags). Note that you can
159 | deploy from every branch with --no-require-master.""", metavar="BRANCH")
160 | deploy_parser_add_argument('--no-require-master', dest='require_master', action='store_false',
161 | default=True, help="""Allow docs to be pushed from a branch other than master""")
162 | deploy_parser_add_argument('--command', default=None,
163 | help="""Command to be run before committing and pushing. This command
164 | will be run from the deploy repository/branch. If the command creates
165 | additional files that should be deployed, they should be added to the
166 | index.""")
167 | deploy_parser_add_argument('--no-sync', dest='sync', action='store_false',
168 | default=True, help="""Don't sync any files. This is generally used in
169 | conjunction with the --command flag, for instance, if the command syncs
170 | the files for you. Any files you wish to commit should be added to the
171 | index.""")
172 | deploy_parser.add_argument('--no-temp-dir', dest='temp_dir',
173 | action='store_false', default=True, help="""Don't copy the
174 | --built-docs directory to a temporary directory.""")
175 | deploy_parser_add_argument('--no-push', dest='push', action='store_false',
176 | default=True, help="Run all the steps except the last push step. "
177 | "Useful for debugging")
178 | deploy_parser_add_argument('--build-tags', action='store_true',
179 | default=False, help="""Deploy on tag builds. On a tag build,
180 | $TRAVIS_TAG is set to the name of the tag. The default is to not
181 | deploy on tag builds. Note that this will still build on a branch,
182 | unless --branch-whitelist (with no arguments) is passed.""")
183 | deploy_parser_add_argument('--gh-pages-docs', default=None,
184 | help="""!!DEPRECATED!! Directory to deploy the html documentation to on gh-pages.
185 | The default is %(default)r. The deploy directory should be passed as
186 | the first argument to 'doctr deploy'. This flag is kept for backwards
187 | compatibility.""")
188 | deploy_parser_add_argument('--exclude', nargs='+', default=(), help="""Files and
189 | directories from --built-docs that are not copied.""")
190 |
191 | if config:
192 | print('Warning, The following options in `.travis.yml` were not recognized:\n%s' % json.dumps(config, indent=2))
193 |
194 | configure_parser = subcommand.add_parser('configure', help="Configure doctr. This command should be run locally (not on Travis).")
195 | configure_parser.set_defaults(func=configure)
196 | configure_parser.add_argument('--force', action='store_true', help="""Run the configure command even
197 | if we appear to be on Travis.""")
198 | configure_parser.add_argument('--token', action="store_true", default=False,
199 | help="""Generate a personal access token to push to GitHub. The default is to use a
200 | deploy key. WARNING: This will grant read/write access to all the
201 | public repositories for the user. This option is not recommended
202 | unless you are using a separate GitHub user for deploying.""")
203 | configure_parser.add_argument("--no-upload-key", action="store_false", default=True,
204 | dest="upload_key", help="""Don't automatically upload the deploy key to GitHub. To prevent doctr
205 | configure from requiring you to login to GitHub, use
206 | --no-authenticate.""")
207 | configure_parser.add_argument("--no-authenticate", action="store_false",
208 | default=True, dest="authenticate", help="""Don't authenticate with GitHub. This option implies --no-upload-key. This
209 | option is also not compatible with private repositories.""")
210 | configure_parser.add_argument('--key-path', default=None,
211 | help="""Path to save the encrypted GitHub deploy key. The default is github_deploy_key_+
212 | deploy respository name. The .enc extension is added to the file automatically.""")
213 | configure_parser.add_argument('--travis-tld', default=None,
214 | help="""Travis tld to use. Should be either '.com' or '.org'. The default is to
215 | check which the repo is activated on and ask if it is activated on
216 | both.""", choices=['c', 'com', '.com', 'travis-ci.com', 'o', 'org', '.org',
217 | 'travis-ci.org'])
218 |
219 | return parser
220 |
221 | def get_config():
222 | """
223 | This load some configuration from the ``.travis.yml``, if file is present,
224 | ``doctr`` key if present.
225 | """
226 | p = Path('.travis.yml')
227 | if not p.exists():
228 | return {}
229 | with p.open() as f:
230 | travis_config = yaml.safe_load(f.read())
231 |
232 | config = travis_config.get('doctr', {})
233 |
234 | if not isinstance(config, dict):
235 | raise ValueError('config is not a dict: {}'.format(config))
236 | return config
237 |
238 | def get_deploy_key_repo(deploy_repo, keypath, key_ext=''):
239 | """
240 | Return (repository of which deploy key is used, environment variable to store
241 | the encryption key of deploy key, path of deploy key file)
242 | """
243 | # deploy key of the original repo has write access to the wiki
244 | deploy_key_repo = deploy_repo[:-5] if deploy_repo.endswith('.wiki') else deploy_repo
245 |
246 | # Automatically determine environment variable and key file name from deploy repo name
247 | # Special characters are substituted with a hyphen(-) by GitHub
248 | snake_case_name = deploy_key_repo.replace('-', '_').replace('.', '_').replace('/', '_').lower()
249 | env_name = 'DOCTR_DEPLOY_ENCRYPTION_KEY_' + snake_case_name.upper()
250 | keypath = keypath or 'github_deploy_key_' + snake_case_name + key_ext
251 |
252 | return (deploy_key_repo, env_name, keypath)
253 |
254 | def process_args(parser):
255 | args = parser.parse_args()
256 |
257 | if not args.subcommand:
258 | parser.print_usage()
259 | parser.exit(1)
260 |
261 | try:
262 | return args.func(args, parser)
263 | except RuntimeError as e:
264 | sys.exit(red("Error: " + e.args[0]))
265 | except KeyboardInterrupt:
266 | sys.exit(red("Interrupted by user"))
267 |
268 | def on_travis():
269 | return os.environ.get("TRAVIS_JOB_NUMBER", '')
270 |
271 | def deploy(args, parser):
272 | print("Running doctr deploy, version", __version__)
273 |
274 | if not args.force and not on_travis():
275 | parser.error("doctr does not appear to be running on Travis. Use "
276 | "doctr deploy --force to run anyway.")
277 |
278 | config = get_config()
279 |
280 | if args.tmp_dir:
281 | parser.error("The --tmp-dir flag has been removed (doctr no longer uses a temporary directory when deploying).")
282 |
283 | if args.gh_pages_docs:
284 | print("The --gh-pages-docs flag is deprecated and will be removed in the next release. Instead pass the deploy directory as an argument, e.g. `doctr deploy .`")
285 |
286 | if args.gh_pages_docs and args.deploy_directory:
287 | parser.error("The --gh-pages-docs flag is deprecated. Specify the directory to deploy to using `doctr deploy `")
288 |
289 | if not args.gh_pages_docs and not args.deploy_directory:
290 | parser.error("No deploy directory specified. Specify the directory to deploy to using `doctr deploy `")
291 |
292 | deploy_dir = args.gh_pages_docs or args.deploy_directory
293 |
294 | build_repo = get_current_repo()
295 | deploy_repo = args.deploy_repo or build_repo
296 |
297 | if args.deploy_branch_name:
298 | deploy_branch = args.deploy_branch_name
299 | else:
300 | deploy_branch = 'master' if deploy_repo.endswith(('.github.io', '.github.com', '.wiki')) else 'gh-pages'
301 |
302 | _, env_name, keypath = get_deploy_key_repo(deploy_repo, args.key_path, key_ext='.enc')
303 |
304 | current_commit = subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8').strip()
305 | try:
306 | branch_whitelist = set() if args.require_master else set(get_travis_branch())
307 | branch_whitelist.update(set(config.get('branches', set())))
308 | if args.branch_whitelist is not None:
309 | branch_whitelist.update(set(args.branch_whitelist))
310 | elif not branch_whitelist:
311 | branch_whitelist = {'master'}
312 |
313 | canpush = setup_GitHub_push(deploy_repo, deploy_branch=deploy_branch,
314 | auth_type='token' if args.token else 'deploy_key',
315 | full_key_path=keypath,
316 | branch_whitelist=branch_whitelist,
317 | build_tags=args.build_tags,
318 | env_name=env_name)
319 |
320 | if args.sync:
321 | built_docs = args.built_docs or find_sphinx_build_dir()
322 | exclude = [os.path.relpath(i, built_docs) for i in args.exclude]
323 | if args.temp_dir:
324 | built_docs = copy_to_tmp(built_docs)
325 | exclude = [os.path.normpath(os.path.join(built_docs, i)) for i in exclude]
326 |
327 | # Reset in case there are modified files that are tracked in the
328 | # deploy branch.
329 | run(['git', 'stash', '--all'])
330 | checkout_deploy_branch(deploy_branch, canpush=canpush)
331 |
332 | if args.sync:
333 | log_file = os.path.join(deploy_dir, '.doctr-files')
334 |
335 | print("Moving built docs into place")
336 | added, removed = sync_from_log(src=built_docs,
337 | dst=deploy_dir, log_file=log_file, exclude=exclude)
338 |
339 | else:
340 | added, removed = [], []
341 |
342 | if args.command:
343 | run(args.command, shell=True)
344 |
345 | changes = commit_docs(added=added, removed=removed)
346 | if changes:
347 | if canpush and args.push:
348 | push_docs(deploy_branch)
349 | else:
350 | print("Don't have permission to push. Not trying.")
351 | else:
352 | print("The docs have not changed. Not updating")
353 | except BaseException as e:
354 | DOCTR_COMMAND = ' '.join(map(shlex.quote, sys.argv))
355 | print(red("ERROR: The doctr command %r failed: %r" % (DOCTR_COMMAND, e)),
356 | file=sys.stderr)
357 | raise
358 | finally:
359 | run(['git', 'checkout', current_commit])
360 | # Ignore error, won't do anything if there was nothing to stash
361 | run(['git', 'stash', 'pop'], exit=False)
362 |
363 | class IncrementingInt:
364 | def __init__(self, i=0):
365 | self.i = i
366 |
367 | def __repr__(self):
368 | ret = repr(self.i)
369 | self.i += 1
370 | return ret
371 |
372 | __str__ = __repr__
373 |
374 | def configure(args, parser):
375 | """
376 | Color guide
377 |
378 | - red: Error and warning messages
379 | - green: Welcome messages (use sparingly)
380 | - blue: Default values
381 | - bold_magenta: Action items
382 | - bold_black: Parts of code to be run or copied that should be modified
383 | """
384 | if not args.force and on_travis():
385 | parser.error(red("doctr appears to be running on Travis. Use "
386 | "doctr configure --force to run anyway."))
387 |
388 | if not args.authenticate:
389 | args.upload_key = False
390 |
391 | if args.travis_tld:
392 | if args.travis_tld in ['c', 'com', '.com', 'travis-ci.com']:
393 | args.travis_tld = 'travis-ci.com'
394 | else:
395 | args.travis_tld = 'travis-ci.org'
396 |
397 | print(green(dedent("""\
398 | Welcome to Doctr.
399 |
400 | We need to ask you a few questions to get you on your way to automatically
401 | deploying from Travis CI to GitHub pages.
402 | """)))
403 |
404 | login_kwargs = {}
405 |
406 | if args.authenticate:
407 | try:
408 | print(bold_magenta("We must first authenticate with GitHub. This authorization is only needed for the initial configuration, and may be revoked after this command exits. The 'repo' scope is used so that I can upload the deploy key to the repo for you. You may also use 'doctr configure --no-authenticate' if you want to configure doctr without authenticating with GitHub (this will require pasting the deploy key into the GitHub form manually).\n"))
409 | access_token = GitHub_login(client_id=DOCTR_CLIENT_ID)
410 | login_kwargs = {'headers': {'Authorization': "token {}".format(access_token)}}
411 | except AuthenticationFailed as e:
412 | sys.exit(red(e))
413 | else:
414 | login_kwargs = {'headers': None}
415 |
416 | GitHub_token = None
417 | get_build_repo = False
418 | default_repo = guess_github_repo()
419 | while not get_build_repo:
420 | try:
421 | if default_repo:
422 | build_repo = input("What repo do you want to build the docs for? [{default_repo}] ".format(default_repo=blue(default_repo)))
423 | if not build_repo:
424 | build_repo = default_repo
425 | else:
426 | build_repo = input("What repo do you want to build the docs for (org/reponame, like 'drdoctr/doctr')? ")
427 |
428 | is_private = check_repo_exists(build_repo, service='github',
429 | **login_kwargs)['private']
430 | if is_private and not args.authenticate:
431 | sys.exit(red("--no-authenticate is not supported for private repositories."))
432 |
433 | headers = {}
434 | travis_token = None
435 | if is_private:
436 | if args.token:
437 | GitHub_token = generate_GitHub_token(note="Doctr token for pushing to gh-pages from Travis (for {build_repo}).".format(build_repo=build_repo),
438 | scopes=["read:org", "user:email", "repo"], **login_kwargs)['token']
439 | travis_token = get_travis_token(GitHub_token=GitHub_token, **login_kwargs)
440 | headers['Authorization'] = "token {}".format(travis_token)
441 |
442 | service = args.travis_tld if args.travis_tld else 'travis'
443 | c = check_repo_exists(build_repo, service=service, ask=True, headers=headers)
444 | tld = c['service'][-4:]
445 | is_private = c['private'] or is_private
446 | if is_private and not args.authenticate:
447 | sys.exit(red("--no-authenticate is not supported for private repos."))
448 |
449 | get_build_repo = True
450 | except GitHubError:
451 | raise
452 | except RuntimeError as e:
453 | print(red('\n{!s:-^{}}\n'.format(e, 70)))
454 |
455 | get_deploy_repo = False
456 | while not get_deploy_repo:
457 | try:
458 | deploy_repo = input("What repo do you want to deploy the docs to? [{build_repo}] ".format(build_repo=blue(build_repo)))
459 | if not deploy_repo:
460 | deploy_repo = build_repo
461 |
462 | if deploy_repo != build_repo:
463 | check_repo_exists(deploy_repo, service='github', **login_kwargs)
464 |
465 | get_deploy_repo = True
466 | except GitHubError:
467 | raise
468 | except RuntimeError as e:
469 | print(red('\n{!s:-^{}}\n'.format(e, 70)))
470 |
471 | N = IncrementingInt(1)
472 |
473 | header = green("\n================== You should now do the following ==================\n")
474 |
475 | if args.token:
476 | if not GitHub_token:
477 | GitHub_token = generate_GitHub_token(**login_kwargs)['token']
478 | encrypted_variable = encrypt_variable("GH_TOKEN={GitHub_token}".format(GitHub_token=GitHub_token).encode('utf-8'),
479 | build_repo=build_repo, tld=tld, travis_token=travis_token, **login_kwargs)
480 | print(dedent("""
481 | A personal access token for doctr has been created.
482 |
483 | You can go to https://github.com/settings/tokens to revoke it."""))
484 |
485 | print(header)
486 | else:
487 | deploy_key_repo, env_name, keypath = get_deploy_key_repo(deploy_repo, args.key_path)
488 |
489 | private_ssh_key, public_ssh_key = generate_ssh_key()
490 | key = encrypt_to_file(private_ssh_key, keypath + '.enc')
491 | del private_ssh_key # Prevent accidental use below
492 | public_ssh_key = public_ssh_key.decode('ASCII')
493 | encrypted_variable = encrypt_variable(env_name.encode('utf-8') + b"=" + key,
494 | build_repo=build_repo, tld=tld, travis_token=travis_token, **login_kwargs)
495 |
496 | deploy_keys_url = 'https://github.com/{deploy_repo}/settings/keys'.format(deploy_repo=deploy_key_repo)
497 |
498 | if args.upload_key:
499 |
500 | upload_GitHub_deploy_key(deploy_key_repo, public_ssh_key, **login_kwargs)
501 |
502 | print(dedent("""
503 | The deploy key has been added for {deploy_repo}.
504 |
505 | You can go to {deploy_keys_url} to revoke the deploy key.\
506 | """.format(deploy_repo=deploy_key_repo, deploy_keys_url=deploy_keys_url)))
507 | print(header)
508 | else:
509 | print(header)
510 | print(dedent("""\
511 | {N}. {BOLD_MAGENTA}Go to {deploy_keys_url}
512 | and add the following as a new key:{RESET}
513 |
514 | {ssh_key}
515 | {BOLD_MAGENTA}Be sure to allow write access for the key.{RESET}
516 | """.format(ssh_key=public_ssh_key, deploy_keys_url=deploy_keys_url, N=N,
517 | BOLD_MAGENTA=BOLD_MAGENTA, RESET=RESET)))
518 |
519 |
520 | print(dedent("""\
521 | {N}. {BOLD_MAGENTA}Add the file {keypath}.enc to be staged for commit:{RESET}
522 |
523 | git add {keypath}.enc
524 | """.format(keypath=keypath, N=N, BOLD_MAGENTA=BOLD_MAGENTA, RESET=RESET)))
525 |
526 | options = '--built-docs ' + bold_black('')
527 | if args.key_path:
528 | options += ' --key-path {keypath}.enc'.format(keypath=keypath)
529 | if deploy_repo != build_repo:
530 | options += ' --deploy-repo {deploy_repo}'.format(deploy_repo=deploy_repo)
531 |
532 | key_type = "deploy key"
533 | if args.token:
534 | options += ' --token'
535 | key_type = "personal access token"
536 |
537 | print(dedent("""\
538 | {N}. {BOLD_MAGENTA}Add these lines to your `.travis.yml` file:{RESET}
539 |
540 | env:
541 | global:
542 | # Doctr {key_type} for {deploy_repo}
543 | - secure: "{encrypted_variable}"
544 |
545 | script:
546 | - set -e
547 | - {BOLD_BLACK}{RESET}
548 | - pip install doctr
549 | - doctr deploy {options} {BOLD_BLACK}{RESET}
550 | """.format(options=options, N=N, key_type=key_type,
551 | encrypted_variable=encrypted_variable.decode('utf-8'),
552 | deploy_repo=deploy_repo, BOLD_MAGENTA=BOLD_MAGENTA,
553 | BOLD_BLACK=BOLD_BLACK, RESET=RESET)))
554 |
555 | print(dedent("""\
556 | Replace the text in {BOLD_BLACK}{RESET} with the relevant
557 | things for your repository.
558 | """.format(BOLD_BLACK=BOLD_BLACK, RESET=RESET)))
559 |
560 | print(dedent("""\
561 | Note: the `set -e` prevents doctr from running when the docs build fails.
562 | We put this code under `script:` so that if doctr fails it causes the
563 | build to fail.
564 | """))
565 |
566 | print(dedent("""\
567 | {N}. {BOLD_MAGENTA}Commit and push these changes to your GitHub repository.{RESET}
568 | The docs should now build automatically on Travis.
569 | """.format(N=N, BOLD_MAGENTA=BOLD_MAGENTA, RESET=RESET)))
570 |
571 | if args.authenticate:
572 | app_url = "https://github.com/settings/connections/applications/" + DOCTR_CLIENT_ID
573 | print(dedent("""\
574 | {N}. {BOLD_MAGENTA}Finally, if you like, you may go to {app_url} and revoke access to the doctr application (it is not needed for doctr to work past this point).{RESET}
575 | """.format(N=N, BOLD_MAGENTA=BOLD_MAGENTA,
576 | app_url=app_url, RESET=RESET)))
577 |
578 | print("See the documentation at https://drdoctr.github.io/ for more information.")
579 |
580 | def main():
581 | config = get_config()
582 | return process_args(get_parser(config=config))
583 |
584 | if __name__ == '__main__':
585 | sys.exit(main())
586 |
--------------------------------------------------------------------------------
/doctr/_version.py:
--------------------------------------------------------------------------------
1 |
2 | # This file helps to compute a version number in source trees obtained from
3 | # git-archive tarball (such as those provided by githubs download-from-tag
4 | # feature). Distribution tarballs (built by setup.py sdist) and build
5 | # directories (produced by setup.py build) will contain a much shorter file
6 | # that just contains the computed version number.
7 |
8 | # This file is released into the public domain. Generated by
9 | # versioneer-0.15 (https://github.com/warner/python-versioneer)
10 |
11 | import errno
12 | import os
13 | import re
14 | import subprocess
15 | import sys
16 |
17 |
18 | def get_keywords():
19 | # these strings will be replaced by git during git-archive.
20 | # setup.py/versioneer.py will grep for the variable names, so they must
21 | # each be defined on a line of their own. _version.py will just call
22 | # get_keywords().
23 | git_refnames = " (HEAD -> master)"
24 | git_full = "7e757118a1807aed5b7e95c5404bd47c6dbdccd0"
25 | keywords = {"refnames": git_refnames, "full": git_full}
26 | return keywords
27 |
28 |
29 | class VersioneerConfig:
30 | pass
31 |
32 |
33 | def get_config():
34 | # these strings are filled in when 'setup.py versioneer' creates
35 | # _version.py
36 | cfg = VersioneerConfig()
37 | cfg.VCS = "git"
38 | cfg.style = "pep440"
39 | cfg.tag_prefix = ""
40 | cfg.parentdir_prefix = ""
41 | cfg.versionfile_source = "doctr/_version.py"
42 | cfg.verbose = False
43 | return cfg
44 |
45 |
46 | class NotThisMethod(Exception):
47 | pass
48 |
49 |
50 | LONG_VERSION_PY = {}
51 | HANDLERS = {}
52 |
53 |
54 | def register_vcs_handler(vcs, method): # decorator
55 | def decorate(f):
56 | if vcs not in HANDLERS:
57 | HANDLERS[vcs] = {}
58 | HANDLERS[vcs][method] = f
59 | return f
60 | return decorate
61 |
62 |
63 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False):
64 | assert isinstance(commands, list)
65 | p = None
66 | for c in commands:
67 | try:
68 | dispcmd = str([c] + args)
69 | # remember shell=False, so use git.cmd on windows, not just git
70 | p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE,
71 | stderr=(subprocess.PIPE if hide_stderr
72 | else None))
73 | break
74 | except EnvironmentError:
75 | e = sys.exc_info()[1]
76 | if e.errno == errno.ENOENT:
77 | continue
78 | if verbose:
79 | print("unable to run %s" % dispcmd)
80 | print(e)
81 | return None
82 | else:
83 | if verbose:
84 | print("unable to find command, tried %s" % (commands,))
85 | return None
86 | stdout = p.communicate()[0].strip()
87 | if sys.version_info[0] >= 3:
88 | stdout = stdout.decode()
89 | if p.returncode != 0:
90 | if verbose:
91 | print("unable to run %s (error)" % dispcmd)
92 | return None
93 | return stdout
94 |
95 |
96 | def versions_from_parentdir(parentdir_prefix, root, verbose):
97 | # Source tarballs conventionally unpack into a directory that includes
98 | # both the project name and a version string.
99 | dirname = os.path.basename(root)
100 | if not dirname.startswith(parentdir_prefix):
101 | if verbose:
102 | print("guessing rootdir is '%s', but '%s' doesn't start with "
103 | "prefix '%s'" % (root, dirname, parentdir_prefix))
104 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
105 | return {"version": dirname[len(parentdir_prefix):],
106 | "full-revisionid": None,
107 | "dirty": False, "error": None}
108 |
109 |
110 | @register_vcs_handler("git", "get_keywords")
111 | def git_get_keywords(versionfile_abs):
112 | # the code embedded in _version.py can just fetch the value of these
113 | # keywords. When used from setup.py, we don't want to import _version.py,
114 | # so we do it with a regexp instead. This function is not used from
115 | # _version.py.
116 | keywords = {}
117 | try:
118 | f = open(versionfile_abs, "r")
119 | for line in f.readlines():
120 | if line.strip().startswith("git_refnames ="):
121 | mo = re.search(r'=\s*"(.*)"', line)
122 | if mo:
123 | keywords["refnames"] = mo.group(1)
124 | if line.strip().startswith("git_full ="):
125 | mo = re.search(r'=\s*"(.*)"', line)
126 | if mo:
127 | keywords["full"] = mo.group(1)
128 | f.close()
129 | except EnvironmentError:
130 | pass
131 | return keywords
132 |
133 |
134 | @register_vcs_handler("git", "keywords")
135 | def git_versions_from_keywords(keywords, tag_prefix, verbose):
136 | if not keywords:
137 | raise NotThisMethod("no keywords at all, weird")
138 | refnames = keywords["refnames"].strip()
139 | if refnames.startswith("$Format"):
140 | if verbose:
141 | print("keywords are unexpanded, not using")
142 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
143 | refs = set([r.strip() for r in refnames.strip("()").split(",")])
144 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
145 | # just "foo-1.0". If we see a "tag: " prefix, prefer those.
146 | TAG = "tag: "
147 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
148 | if not tags:
149 | # Either we're using git < 1.8.3, or there really are no tags. We use
150 | # a heuristic: assume all version tags have a digit. The old git %d
151 | # expansion behaves like git log --decorate=short and strips out the
152 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish
153 | # between branches and tags. By ignoring refnames without digits, we
154 | # filter out many common branch names like "release" and
155 | # "stabilization", as well as "HEAD" and "master".
156 | tags = set([r for r in refs if re.search(r'\d', r)])
157 | if verbose:
158 | print("discarding '%s', no digits" % ",".join(refs-tags))
159 | if verbose:
160 | print("likely tags: %s" % ",".join(sorted(tags)))
161 | for ref in sorted(tags):
162 | # sorting will prefer e.g. "2.0" over "2.0rc1"
163 | if ref.startswith(tag_prefix):
164 | r = ref[len(tag_prefix):]
165 | if verbose:
166 | print("picking %s" % r)
167 | return {"version": r,
168 | "full-revisionid": keywords["full"].strip(),
169 | "dirty": False, "error": None
170 | }
171 | # no suitable tags, so version is "0+unknown", but full hex is still there
172 | if verbose:
173 | print("no suitable tags, using unknown + full revision id")
174 | return {"version": "0+unknown",
175 | "full-revisionid": keywords["full"].strip(),
176 | "dirty": False, "error": "no suitable tags"}
177 |
178 |
179 | @register_vcs_handler("git", "pieces_from_vcs")
180 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
181 | # this runs 'git' from the root of the source tree. This only gets called
182 | # if the git-archive 'subst' keywords were *not* expanded, and
183 | # _version.py hasn't already been rewritten with a short version string,
184 | # meaning we're inside a checked out source tree.
185 |
186 | if not os.path.exists(os.path.join(root, ".git")):
187 | if verbose:
188 | print("no .git in %s" % root)
189 | raise NotThisMethod("no .git directory")
190 |
191 | GITS = ["git"]
192 | if sys.platform == "win32":
193 | GITS = ["git.cmd", "git.exe"]
194 | # if there is a tag, this yields TAG-NUM-gHEX[-dirty]
195 | # if there are no tags, this yields HEX[-dirty] (no NUM)
196 | describe_out = run_command(GITS, ["describe", "--tags", "--dirty",
197 | "--always", "--long"],
198 | cwd=root)
199 | # --long was added in git-1.5.5
200 | if describe_out is None:
201 | raise NotThisMethod("'git describe' failed")
202 | describe_out = describe_out.strip()
203 | full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
204 | if full_out is None:
205 | raise NotThisMethod("'git rev-parse' failed")
206 | full_out = full_out.strip()
207 |
208 | pieces = {}
209 | pieces["long"] = full_out
210 | pieces["short"] = full_out[:7] # maybe improved later
211 | pieces["error"] = None
212 |
213 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
214 | # TAG might have hyphens.
215 | git_describe = describe_out
216 |
217 | # look for -dirty suffix
218 | dirty = git_describe.endswith("-dirty")
219 | pieces["dirty"] = dirty
220 | if dirty:
221 | git_describe = git_describe[:git_describe.rindex("-dirty")]
222 |
223 | # now we have TAG-NUM-gHEX or HEX
224 |
225 | if "-" in git_describe:
226 | # TAG-NUM-gHEX
227 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
228 | if not mo:
229 | # unparseable. Maybe git-describe is misbehaving?
230 | pieces["error"] = ("unable to parse git-describe output: '%s'"
231 | % describe_out)
232 | return pieces
233 |
234 | # tag
235 | full_tag = mo.group(1)
236 | if not full_tag.startswith(tag_prefix):
237 | if verbose:
238 | fmt = "tag '%s' doesn't start with prefix '%s'"
239 | print(fmt % (full_tag, tag_prefix))
240 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
241 | % (full_tag, tag_prefix))
242 | return pieces
243 | pieces["closest-tag"] = full_tag[len(tag_prefix):]
244 |
245 | # distance: number of commits since tag
246 | pieces["distance"] = int(mo.group(2))
247 |
248 | # commit: short hex revision ID
249 | pieces["short"] = mo.group(3)
250 |
251 | else:
252 | # HEX: no tags
253 | pieces["closest-tag"] = None
254 | count_out = run_command(GITS, ["rev-list", "HEAD", "--count"],
255 | cwd=root)
256 | pieces["distance"] = int(count_out) # total number of commits
257 |
258 | return pieces
259 |
260 |
261 | def plus_or_dot(pieces):
262 | if "+" in pieces.get("closest-tag", ""):
263 | return "."
264 | return "+"
265 |
266 |
267 | def render_pep440(pieces):
268 | # now build up version string, with post-release "local version
269 | # identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
270 | # get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
271 |
272 | # exceptions:
273 | # 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
274 |
275 | if pieces["closest-tag"]:
276 | rendered = pieces["closest-tag"]
277 | if pieces["distance"] or pieces["dirty"]:
278 | rendered += plus_or_dot(pieces)
279 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
280 | if pieces["dirty"]:
281 | rendered += ".dirty"
282 | else:
283 | # exception #1
284 | rendered = "0+untagged.%d.g%s" % (pieces["distance"],
285 | pieces["short"])
286 | if pieces["dirty"]:
287 | rendered += ".dirty"
288 | return rendered
289 |
290 |
291 | def render_pep440_pre(pieces):
292 | # TAG[.post.devDISTANCE] . No -dirty
293 |
294 | # exceptions:
295 | # 1: no tags. 0.post.devDISTANCE
296 |
297 | if pieces["closest-tag"]:
298 | rendered = pieces["closest-tag"]
299 | if pieces["distance"]:
300 | rendered += ".post.dev%d" % pieces["distance"]
301 | else:
302 | # exception #1
303 | rendered = "0.post.dev%d" % pieces["distance"]
304 | return rendered
305 |
306 |
307 | def render_pep440_post(pieces):
308 | # TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that
309 | # .dev0 sorts backwards (a dirty tree will appear "older" than the
310 | # corresponding clean one), but you shouldn't be releasing software with
311 | # -dirty anyways.
312 |
313 | # exceptions:
314 | # 1: no tags. 0.postDISTANCE[.dev0]
315 |
316 | if pieces["closest-tag"]:
317 | rendered = pieces["closest-tag"]
318 | if pieces["distance"] or pieces["dirty"]:
319 | rendered += ".post%d" % pieces["distance"]
320 | if pieces["dirty"]:
321 | rendered += ".dev0"
322 | rendered += plus_or_dot(pieces)
323 | rendered += "g%s" % pieces["short"]
324 | else:
325 | # exception #1
326 | rendered = "0.post%d" % pieces["distance"]
327 | if pieces["dirty"]:
328 | rendered += ".dev0"
329 | rendered += "+g%s" % pieces["short"]
330 | return rendered
331 |
332 |
333 | def render_pep440_old(pieces):
334 | # TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty.
335 |
336 | # exceptions:
337 | # 1: no tags. 0.postDISTANCE[.dev0]
338 |
339 | if pieces["closest-tag"]:
340 | rendered = pieces["closest-tag"]
341 | if pieces["distance"] or pieces["dirty"]:
342 | rendered += ".post%d" % pieces["distance"]
343 | if pieces["dirty"]:
344 | rendered += ".dev0"
345 | else:
346 | # exception #1
347 | rendered = "0.post%d" % pieces["distance"]
348 | if pieces["dirty"]:
349 | rendered += ".dev0"
350 | return rendered
351 |
352 |
353 | def render_git_describe(pieces):
354 | # TAG[-DISTANCE-gHEX][-dirty], like 'git describe --tags --dirty
355 | # --always'
356 |
357 | # exceptions:
358 | # 1: no tags. HEX[-dirty] (note: no 'g' prefix)
359 |
360 | if pieces["closest-tag"]:
361 | rendered = pieces["closest-tag"]
362 | if pieces["distance"]:
363 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
364 | else:
365 | # exception #1
366 | rendered = pieces["short"]
367 | if pieces["dirty"]:
368 | rendered += "-dirty"
369 | return rendered
370 |
371 |
372 | def render_git_describe_long(pieces):
373 | # TAG-DISTANCE-gHEX[-dirty], like 'git describe --tags --dirty
374 | # --always -long'. The distance/hash is unconditional.
375 |
376 | # exceptions:
377 | # 1: no tags. HEX[-dirty] (note: no 'g' prefix)
378 |
379 | if pieces["closest-tag"]:
380 | rendered = pieces["closest-tag"]
381 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
382 | else:
383 | # exception #1
384 | rendered = pieces["short"]
385 | if pieces["dirty"]:
386 | rendered += "-dirty"
387 | return rendered
388 |
389 |
390 | def render(pieces, style):
391 | if pieces["error"]:
392 | return {"version": "unknown",
393 | "full-revisionid": pieces.get("long"),
394 | "dirty": None,
395 | "error": pieces["error"]}
396 |
397 | if not style or style == "default":
398 | style = "pep440" # the default
399 |
400 | if style == "pep440":
401 | rendered = render_pep440(pieces)
402 | elif style == "pep440-pre":
403 | rendered = render_pep440_pre(pieces)
404 | elif style == "pep440-post":
405 | rendered = render_pep440_post(pieces)
406 | elif style == "pep440-old":
407 | rendered = render_pep440_old(pieces)
408 | elif style == "git-describe":
409 | rendered = render_git_describe(pieces)
410 | elif style == "git-describe-long":
411 | rendered = render_git_describe_long(pieces)
412 | else:
413 | raise ValueError("unknown style '%s'" % style)
414 |
415 | return {"version": rendered, "full-revisionid": pieces["long"],
416 | "dirty": pieces["dirty"], "error": None}
417 |
418 |
419 | def get_versions():
420 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
421 | # __file__, we can work backwards from there to the root. Some
422 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
423 | # case we can only use expanded keywords.
424 |
425 | cfg = get_config()
426 | verbose = cfg.verbose
427 |
428 | try:
429 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
430 | verbose)
431 | except NotThisMethod:
432 | pass
433 |
434 | try:
435 | root = os.path.realpath(__file__)
436 | # versionfile_source is the relative path from the top of the source
437 | # tree (where the .git directory might live) to this file. Invert
438 | # this to find the root from __file__.
439 | for i in cfg.versionfile_source.split('/'):
440 | root = os.path.dirname(root)
441 | except NameError:
442 | return {"version": "0+unknown", "full-revisionid": None,
443 | "dirty": None,
444 | "error": "unable to find root of source tree"}
445 |
446 | try:
447 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
448 | return render(pieces, cfg.style)
449 | except NotThisMethod:
450 | pass
451 |
452 | try:
453 | if cfg.parentdir_prefix:
454 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
455 | except NotThisMethod:
456 | pass
457 |
458 | return {"version": "0+unknown", "full-revisionid": None,
459 | "dirty": None,
460 | "error": "unable to compute version"}
461 |
--------------------------------------------------------------------------------
/doctr/common.py:
--------------------------------------------------------------------------------
1 | """
2 | Code used for both Travis and local (deploy and configure)
3 | """
4 |
5 | # Color guide
6 | #
7 | # - red: Error and warning messages
8 | # - green: Welcome messages (use sparingly)
9 | # - yellow: warning message (only use on Travis)
10 | # - blue: Default values
11 | # - bold_magenta: Action items
12 | # - bold_black: Parts of code to be run or copied that should be modified
13 |
14 |
15 | def red(text):
16 | return "\033[31m%s\033[0m" % text
17 |
18 | def green(text):
19 | return "\033[32m%s\033[0m" % text
20 |
21 | def yellow(text):
22 | return "\033[33m%s\033[0m" % text
23 |
24 | def blue(text):
25 | return "\033[34m%s\033[0m" % text
26 |
27 | def bold_black(text):
28 | return "\033[1;30m%s\033[0m" % text
29 |
30 | def bold_magenta(text):
31 | return "\033[1;35m%s\033[0m" % text
32 |
33 | def bold(text):
34 | return "\033[1m%s\033[0m" % text
35 |
36 | # Use these when coloring individual parts of a larger string, e.g.,
37 | # "{BOLD_MAGENTA}Bright text{RESET} normal text".format(BOLD_MAGENTA=BOLD_MAGENTA, RESET=RESET)
38 | BOLD_BLACK = "\033[1;30m"
39 | BOLD_MAGENTA = "\033[1;35m"
40 | RESET = "\033[0m"
41 |
42 | # Remove whitespace on inputs
43 | _input = input
44 | def input(prompt=None):
45 | res = _input(prompt)
46 | return res.strip()
47 |
--------------------------------------------------------------------------------
/doctr/local.py:
--------------------------------------------------------------------------------
1 | """
2 | The code that should be run locally
3 | """
4 |
5 | import json
6 | import uuid
7 | import base64
8 | import subprocess
9 | import re
10 | import urllib
11 | import datetime
12 | import time
13 | import webbrowser
14 |
15 | import requests
16 |
17 | from cryptography.fernet import Fernet
18 |
19 | from cryptography.hazmat.primitives.asymmetric import padding, rsa
20 | from cryptography.hazmat.backends import default_backend
21 | from cryptography.hazmat.primitives import serialization
22 |
23 | from .common import red, blue, green, bold, input
24 |
25 | Travis_APIv2 = {'Accept': 'application/vnd.travis-ci.2.1+json'}
26 | Travis_APIv3 = {"Travis-API-Version": "3"}
27 |
28 | def encrypt_variable(variable, build_repo, *, tld='.org', public_key=None,
29 | travis_token=None, **login_kwargs):
30 | """
31 | Encrypt an environment variable for ``build_repo`` for Travis
32 |
33 | ``variable`` should be a bytes object, of the form ``b'ENV=value'``.
34 |
35 | ``build_repo`` is the repo that ``doctr deploy`` will be run from. It
36 | should be like 'drdoctr/doctr'.
37 |
38 | ``tld`` should be ``'.org'`` for travis-ci.org and ``'.com'`` for
39 | travis-ci.com.
40 |
41 | ``public_key`` should be a pem format public key, obtained from Travis if
42 | not provided.
43 |
44 | If the repo is private, travis_token should be as returned by
45 | ``get_temporary_token(**login_kwargs)``. A token being present
46 | automatically implies ``tld='.com'``.
47 |
48 | """
49 | if not isinstance(variable, bytes):
50 | raise TypeError("variable should be bytes")
51 |
52 | if not b"=" in variable:
53 | raise ValueError("variable should be of the form 'VARIABLE=value'")
54 |
55 | if not public_key:
56 | _headers = {
57 | 'Content-Type': 'application/json',
58 | 'User-Agent': 'MyClient/1.0.0',
59 | }
60 | headersv2 = {**_headers, **Travis_APIv2}
61 | headersv3 = {**_headers, **Travis_APIv3}
62 | if travis_token:
63 | headersv3['Authorization'] = 'token {}'.format(travis_token)
64 | res = requests.get('https://api.travis-ci.com/repo/{build_repo}/key_pair/generated'.format(build_repo=urllib.parse.quote(build_repo,
65 | safe='')), headers=headersv3)
66 | if res.json().get('file') == 'not found':
67 | raise RuntimeError("Could not find the Travis public key for %s" % build_repo)
68 | public_key = res.json()['public_key']
69 | else:
70 | res = requests.get('https://api.travis-ci{tld}/repos/{build_repo}/key'.format(build_repo=build_repo,
71 | tld=tld),
72 | headers=headersv2)
73 | public_key = res.json()['key']
74 |
75 | if res.status_code == requests.codes.not_found:
76 | raise RuntimeError('Could not find requested repo on Travis. Is Travis enabled?')
77 | res.raise_for_status()
78 |
79 | public_key = public_key.replace("RSA PUBLIC KEY", "PUBLIC KEY").encode('utf-8')
80 | key = serialization.load_pem_public_key(public_key, backend=default_backend())
81 |
82 | pad = padding.PKCS1v15()
83 |
84 | return base64.b64encode(key.encrypt(variable, pad))
85 |
86 | def encrypt_to_file(contents, filename):
87 | """
88 | Encrypts ``contents`` and writes it to ``filename``.
89 |
90 | ``contents`` should be a bytes string. ``filename`` should end with
91 | ``.enc``.
92 |
93 | Returns the secret key used for the encryption.
94 |
95 | Decrypt the file with :func:`doctr.travis.decrypt_file`.
96 |
97 | """
98 | if not filename.endswith('.enc'):
99 | raise ValueError("%s does not end with .enc" % filename)
100 |
101 | key = Fernet.generate_key()
102 | fer = Fernet(key)
103 |
104 | encrypted_file = fer.encrypt(contents)
105 |
106 | with open(filename, 'wb') as f:
107 | f.write(encrypted_file)
108 |
109 | return key
110 |
111 | class AuthenticationFailed(Exception):
112 | pass
113 |
114 | def GitHub_login(client_id, *, headers=None, scope='repo'):
115 | """
116 | Login to GitHub.
117 |
118 | This uses the device authorization flow. client_id should be the client id
119 | for your GitHub application. See
120 | https://docs.github.com/en/free-pro-team@latest/developers/apps/authorizing-oauth-apps#device-flow.
121 |
122 | 'scope' should be the scope for the access token ('repo' by default). See https://docs.github.com/en/free-pro-team@latest/developers/apps/scopes-for-oauth-apps#available-scopes.
123 |
124 | Returns an access token.
125 |
126 | """
127 | _headers = headers or {}
128 | headers = {"accept": "application/json", **_headers}
129 |
130 | r = requests.post("https://github.com/login/device/code",
131 | {"client_id": client_id, "scope": scope},
132 | headers=headers)
133 | GitHub_raise_for_status(r)
134 | result = r.json()
135 | device_code = result['device_code']
136 | user_code = result['user_code']
137 | verification_uri = result['verification_uri']
138 | expires_in = result['expires_in']
139 | interval = result['interval']
140 | request_time = time.time()
141 |
142 | print("Go to", verification_uri, "and enter this code:")
143 | print()
144 | print(bold(user_code))
145 | print()
146 | input("Press Enter to open a webbrowser to " + verification_uri)
147 | webbrowser.open(verification_uri)
148 | while True:
149 | time.sleep(interval)
150 | now = time.time()
151 | if now - request_time > expires_in:
152 | print("Did not receive a response in time. Please try again.")
153 | return GitHub_login(client_id=client_id, headers=headers, scope=scope)
154 | # Try once before opening in case the user already did it
155 | r = requests.post("https://github.com/login/oauth/access_token",
156 | {"client_id": client_id,
157 | "device_code": device_code,
158 | "grant_type": "urn:ietf:params:oauth:grant-type:device_code"},
159 | headers=headers)
160 | GitHub_raise_for_status(r)
161 | result = r.json()
162 | if "error" in result:
163 | # https://docs.github.com/en/free-pro-team@latest/developers/apps/authorizing-oauth-apps#error-codes-for-the-device-flow
164 | error = result['error']
165 | if error == "authorization_pending":
166 | if 0:
167 | print("No response from GitHub yet: trying again")
168 | continue
169 | elif error == "slow_down":
170 | # We are polling too fast somehow. This adds 5 seconds to the
171 | # poll interval, which we increase by 6 just to be sure it
172 | # doesn't happen again.
173 | interval += 6
174 | continue
175 | elif error == "expired_token":
176 | print("GitHub token expired. Trying again...")
177 | return GitHub_login(client_id=client_id, headers=headers, scope=scope)
178 | elif error == "access_denied":
179 | raise AuthenticationFailed("User canceled authorization")
180 | else:
181 | # The remaining errors, "unsupported_grant_type",
182 | # "incorrect_client_credentials", and "incorrect_device_code"
183 | # mean the above request was incorrect somehow, which
184 | # indicates a bug. Or GitHub added a new error type, in which
185 | # case this code needs to be updated.
186 | raise AuthenticationFailed("Unexpected error when authorizing with GitHub:", error)
187 | else:
188 | return result['access_token']
189 |
190 |
191 | class GitHubError(RuntimeError):
192 | pass
193 |
194 | def GitHub_raise_for_status(r):
195 | """
196 | Call instead of r.raise_for_status() for GitHub requests
197 |
198 | Checks for common GitHub response issues and prints messages for them.
199 | """
200 | # This will happen if the doctr session has been running too long and the
201 | # OTP code gathered from GitHub_login has expired.
202 |
203 | # TODO: Refactor the code to re-request the OTP without exiting.
204 | if r.status_code == 401 and r.headers.get('X-GitHub-OTP'):
205 | raise GitHubError("The two-factor authentication code has expired. Please run doctr configure again.")
206 | if r.status_code == 403 and r.headers.get('X-RateLimit-Remaining') == '0':
207 | reset = int(r.headers['X-RateLimit-Reset'])
208 | limit = int(r.headers['X-RateLimit-Limit'])
209 | reset_datetime = datetime.datetime.fromtimestamp(reset, datetime.timezone.utc)
210 | relative_reset_datetime = reset_datetime - datetime.datetime.now(datetime.timezone.utc)
211 | # Based on datetime.timedelta.__str__
212 | mm, ss = divmod(relative_reset_datetime.seconds, 60)
213 | hh, mm = divmod(mm, 60)
214 | def plural(n):
215 | return n, abs(n) != 1 and "s" or ""
216 |
217 | s = "%d minute%s" % plural(mm)
218 | if hh:
219 | s = "%d hour%s, " % plural(hh) + s
220 | if relative_reset_datetime.days:
221 | s = ("%d day%s, " % plural(relative_reset_datetime.days)) + s
222 | authenticated = limit >= 100
223 | message = """\
224 | Your GitHub API rate limit has been hit. GitHub allows {limit} {un}authenticated
225 | requests per hour. See {documentation_url}
226 | for more information.
227 | """.format(limit=limit, un="" if authenticated else "un", documentation_url=r.json()["documentation_url"])
228 | if authenticated:
229 | message += """
230 | Note that GitHub's API limits are shared across all oauth applications. A
231 | common cause of hitting the rate limit is the Travis "sync account" button.
232 | """
233 | else:
234 | message += """
235 | You can get a higher API limit by authenticating. Try running doctr configure
236 | again without the --no-upload-key flag.
237 | """
238 | message += """
239 | Your rate limits will reset in {s}.\
240 | """.format(s=s)
241 | raise GitHubError(message)
242 | r.raise_for_status()
243 |
244 |
245 | def GitHub_post(data, url, *, headers):
246 | """
247 | POST the data ``data`` to GitHub.
248 |
249 | Returns the json response from the server, or raises on error status.
250 |
251 | """
252 | r = requests.post(url, headers=headers, data=json.dumps(data))
253 | GitHub_raise_for_status(r)
254 | return r.json()
255 |
256 |
257 | def get_travis_token(*, GitHub_token=None, **login_kwargs):
258 | """
259 | Generate a temporary token for authenticating with Travis
260 |
261 | The GitHub token can be passed in to the ``GitHub_token`` keyword
262 | argument. If no token is passed in, a GitHub token is generated
263 | temporarily, and then immediately deleted.
264 |
265 | This is needed to activate a private repo
266 |
267 | Returns the secret token. It should be added to the headers like
268 |
269 | headers['Authorization'] = "token {}".format(token)
270 |
271 | """
272 | _headers = {
273 | 'Content-Type': 'application/json',
274 | 'User-Agent': 'MyClient/1.0.0',
275 | }
276 | headersv2 = {**_headers, **Travis_APIv2}
277 | token_id = None
278 | try:
279 | if not GitHub_token:
280 | print(green("I need to generate a temporary token with GitHub to authenticate with Travis. You may get a warning email from GitHub about this."))
281 | print(green("It will be deleted immediately. If you still see it after this at https://github.com/settings/tokens after please delete it manually."))
282 | # /auth/github doesn't seem to exist in the Travis API v3.
283 | tok_dict = generate_GitHub_token(scopes=["read:org", "user:email", "repo"],
284 | note="temporary token for doctr to auth against travis (delete me)",
285 | **login_kwargs)
286 | GitHub_token = tok_dict['token']
287 | token_id = tok_dict['id']
288 |
289 | data = {'github_token': GitHub_token}
290 | res = requests.post('https://api.travis-ci.com/auth/github', data=json.dumps(data), headers=headersv2)
291 | return res.json()['access_token']
292 | finally:
293 | if token_id:
294 | delete_GitHub_token(token_id, **login_kwargs)
295 |
296 |
297 | def generate_GitHub_token(*, note="Doctr token for pushing to gh-pages from Travis", scopes=None, **login_kwargs):
298 | """
299 | Generate a GitHub token for pushing from Travis
300 |
301 | The scope requested is public_repo.
302 |
303 | If no password or OTP are provided, they will be requested from the
304 | command line.
305 |
306 | The token created here can be revoked at
307 | https://github.com/settings/tokens.
308 | """
309 | if scopes is None:
310 | scopes = ['public_repo']
311 | AUTH_URL = "https://api.github.com/authorizations"
312 | data = {
313 | "scopes": scopes,
314 | "note": note,
315 | "note_url": "https://github.com/drdoctr/doctr",
316 | "fingerprint": str(uuid.uuid4()),
317 | }
318 | return GitHub_post(data, AUTH_URL, **login_kwargs)
319 |
320 |
321 | def delete_GitHub_token(token_id, *, headers):
322 | """Delete a temporary GitHub token"""
323 | r = requests.delete('https://api.github.com/authorizations/{id}'.format(id=token_id), headers=headers)
324 | GitHub_raise_for_status(r)
325 |
326 |
327 | def upload_GitHub_deploy_key(deploy_repo, ssh_key, *, read_only=False,
328 | title="Doctr deploy key for pushing to gh-pages from Travis", **login_kwargs):
329 | """
330 | Uploads a GitHub deploy key to ``deploy_repo``.
331 |
332 | If ``read_only=True``, the deploy_key will not be able to write to the
333 | repo.
334 | """
335 | DEPLOY_KEY_URL = "https://api.github.com/repos/{deploy_repo}/keys".format(deploy_repo=deploy_repo)
336 |
337 | data = {
338 | "title": title,
339 | "key": ssh_key,
340 | "read_only": read_only,
341 | }
342 | return GitHub_post(data, DEPLOY_KEY_URL, **login_kwargs)
343 |
344 | def generate_ssh_key():
345 | """
346 | Generates an SSH deploy public and private key.
347 |
348 | Returns (private key, public key), a tuple of byte strings.
349 | """
350 |
351 | key = rsa.generate_private_key(
352 | backend=default_backend(),
353 | public_exponent=65537,
354 | key_size=4096
355 | )
356 | private_key = key.private_bytes(
357 | serialization.Encoding.PEM,
358 | serialization.PrivateFormat.PKCS8,
359 | serialization.NoEncryption())
360 | public_key = key.public_key().public_bytes(
361 | serialization.Encoding.OpenSSH,
362 | serialization.PublicFormat.OpenSSH
363 | )
364 |
365 | return private_key, public_key
366 |
367 | def check_repo_exists(deploy_repo, service='github', *, headers=None,
368 | ask=False):
369 | """
370 | Checks that the repository exists on GitHub.
371 |
372 | This should be done before attempting generate a key to deploy to that
373 | repo.
374 |
375 | Raises ``RuntimeError`` if the repo is not valid.
376 |
377 | Returns a dictionary with the following keys:
378 |
379 | - 'private': Indicates whether or not the repo requires authorization to
380 | access. Private repos require authorization.
381 | - 'service': For service='travis', is 'travis-ci.com' or 'travis-ci.org',
382 | depending on which should be used. Otherwise it is just equal to ``service``.
383 |
384 | For service='travis', if ask=True, it will ask at the command line if both
385 | travis-ci.org and travis-ci.com exist. If ask=False, service='travis' will
386 | check travis-ci.com first and only check travis-ci.org if it doesn't
387 | exist. ask=True does nothing for service='github',
388 | service='travis-ci.com', service='travis-ci.org'.
389 |
390 | """
391 | headers = headers or {}
392 | if deploy_repo.count("/") != 1:
393 | raise RuntimeError('"{deploy_repo}" should be in the form username/repo'.format(deploy_repo=deploy_repo))
394 |
395 | user, repo = deploy_repo.split('/')
396 | if service == 'github':
397 | REPO_URL = 'https://api.github.com/repos/{user}/{repo}'
398 | elif service == 'travis' or service == 'travis-ci.com':
399 | REPO_URL = 'https://api.travis-ci.com/repo/{user}%2F{repo}'
400 | headers = {**headers, **Travis_APIv3}
401 | elif service == 'travis-ci.org':
402 | REPO_URL = 'https://api.travis-ci.org/repo/{user}%2F{repo}'
403 | headers = {**headers, **Travis_APIv3}
404 | else:
405 | raise RuntimeError('Invalid service specified for repo check (should be one of {"github", "travis", "travis-ci.com", "travis-ci.org"}')
406 |
407 | wiki = False
408 | if repo.endswith('.wiki') and service == 'github':
409 | wiki = True
410 | repo = repo[:-5]
411 |
412 | def _try(url):
413 | r = requests.get(url, headers=headers)
414 |
415 | if r.status_code in [requests.codes.not_found, requests.codes.forbidden]:
416 | return False
417 | if service == 'github':
418 | GitHub_raise_for_status(r)
419 | else:
420 | r.raise_for_status()
421 | return r
422 |
423 | r = _try(REPO_URL.format(user=urllib.parse.quote(user),
424 | repo=urllib.parse.quote(repo)))
425 | r_active = r and (service == 'github' or r.json().get('active', False))
426 |
427 | if service == 'travis':
428 | REPO_URL = 'https://api.travis-ci.org/repo/{user}%2F{repo}'
429 | r_org = _try(REPO_URL.format(user=urllib.parse.quote(user),
430 | repo=urllib.parse.quote(repo)))
431 | r_org_active = r_org and r_org.json().get('active', False)
432 | if not r_active:
433 | if not r_org_active:
434 | raise RuntimeError('"{user}/{repo}" not found on travis-ci.org or travis-ci.com'.format(user=user, repo=repo))
435 | r = r_org
436 | r_active = r_org_active
437 | service = 'travis-ci.org'
438 | else:
439 | if r_active and r_org_active:
440 | if ask:
441 | while True:
442 | print(green("{user}/{repo} appears to exist on both travis-ci.org and travis-ci.com.".format(user=user, repo=repo)))
443 | preferred = input("Which do you want to use? [{default}/travis-ci.org] ".format(default=blue("travis-ci.com")))
444 | preferred = preferred.lower().strip()
445 | if preferred in ['o', 'org', '.org', 'travis-ci.org']:
446 | r = r_org
447 | service = 'travis-ci.org'
448 | break
449 | elif preferred in ['c', 'com', '.com', 'travis-ci.com', '']:
450 | service = 'travis-ci.com'
451 | break
452 | else:
453 | print(red("Please type 'travis-ci.com' or 'travis-ci.org'."))
454 | else:
455 | service = 'travis-ci.com'
456 | else:
457 | # .com but not .org.
458 | service = 'travis-ci.com'
459 |
460 | if not r_active:
461 | msg = '' if 'Authorization' in headers else '. If the repo is private, then you need to authenticate.'
462 | raise RuntimeError('"{user}/{repo}" not found on {service}{msg}'.format(user=user,
463 | repo=repo,
464 | service=service,
465 | msg=msg))
466 |
467 | private = r.json().get('private', False)
468 |
469 | if wiki and not private:
470 | # private wiki needs authentication, so skip check for existence
471 | p = subprocess.run(['git', 'ls-remote', '-h', 'https://github.com/{user}/{repo}.wiki'.format(
472 | user=user, repo=repo)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
473 | if p.stderr or p.returncode:
474 | raise RuntimeError('Wiki not found. Please create a wiki')
475 |
476 | return {
477 | 'private': private,
478 | 'service': service,
479 | }
480 |
481 | GIT_URL = re.compile(r'(?:git@|https://|git://)github\.com[:/](.*?)(?:\.git)?')
482 |
483 | def guess_github_repo():
484 | """
485 | Guesses the github repo for the current directory
486 |
487 | Returns False if no guess can be made.
488 | """
489 | p = subprocess.run(['git', 'ls-remote', '--get-url', 'origin'],
490 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
491 | if p.stderr or p.returncode:
492 | return False
493 |
494 | url = p.stdout.decode('utf-8').strip()
495 | m = GIT_URL.fullmatch(url)
496 | if not m:
497 | return False
498 | return m.group(1)
499 |
--------------------------------------------------------------------------------
/doctr/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drdoctr/doctr/7e757118a1807aed5b7e95c5404bd47c6dbdccd0/doctr/tests/__init__.py
--------------------------------------------------------------------------------
/doctr/tests/test_local.py:
--------------------------------------------------------------------------------
1 | import os
2 | import binascii
3 |
4 | from ..local import check_repo_exists, GIT_URL, guess_github_repo
5 | from ..__main__ import on_travis
6 |
7 | import pytest
8 | from pytest import raises
9 |
10 | TEST_TOKEN = os.environ.get('TESTING_TOKEN', None)
11 | if TEST_TOKEN:
12 | HEADERS = {'Authorization': 'token {}'.format(TEST_TOKEN)}
13 | else:
14 | HEADERS = None
15 |
16 |
17 | @pytest.mark.skipif(not TEST_TOKEN, reason="No API token present")
18 | def test_github_bad_user():
19 | with raises(RuntimeError):
20 | check_repo_exists('---/invaliduser', headers=HEADERS)
21 |
22 | @pytest.mark.skipif(not TEST_TOKEN, reason="No API token present")
23 | def test_github_bad_repo():
24 | with raises(RuntimeError):
25 | check_repo_exists('drdoctr/---', headers=HEADERS)
26 | with raises(RuntimeError):
27 | check_repo_exists('drdoctr/---.wiki', headers=HEADERS)
28 |
29 | @pytest.mark.skipif(not TEST_TOKEN, reason="No API token present")
30 | def test_github_repo_exists():
31 | assert check_repo_exists('drdoctr/doctr', headers=HEADERS) == {'private': False, 'service': 'github'}
32 | assert check_repo_exists('drdoctr/doctr.wiki', headers=HEADERS) == {'private': False, 'service': 'github'}
33 |
34 | @pytest.mark.parametrize('service', ['travis', 'travis-ci.org', 'travis-ci.com'])
35 | @pytest.mark.parametrize('repo', ['com', 'org', 'both', 'neither'])
36 | def test_check_repo_exists_org_com(repo, service):
37 | deploy_repo = 'drdoctr/testing-travis-ci-' + repo
38 | if (repo == 'neither' or
39 | repo == 'org' and service == 'travis-ci.com' or
40 | repo == 'com' and service == 'travis-ci.org'):
41 | with raises(RuntimeError):
42 | check_repo_exists(deploy_repo, service)
43 | elif (repo == 'org' or
44 | repo == 'both' and service == 'travis-ci.org'):
45 | assert check_repo_exists(deploy_repo, service) == {'private': False,
46 | 'service': 'travis-ci.org'}
47 | else:
48 | assert check_repo_exists(deploy_repo, service) == {'private': False,
49 | 'service': 'travis-ci.com'}
50 |
51 | @pytest.mark.skipif(not TEST_TOKEN, reason="No API token present")
52 | def test_github_invalid_repo():
53 | with raises(RuntimeError):
54 | check_repo_exists('fdsf', headers=HEADERS)
55 |
56 | with raises(RuntimeError):
57 | check_repo_exists('fdsf/fdfs/fd', headers=HEADERS)
58 |
59 | def test_travis_bad_user():
60 | with raises(RuntimeError):
61 | rand_hex = binascii.hexlify(os.urandom(32)).decode('utf-8')
62 | check_repo_exists('drdoctr{}/doctr'.format(rand_hex), service='travis')
63 |
64 | def test_travis_bad_repo():
65 | with raises(RuntimeError):
66 | rand_hex = binascii.hexlify(os.urandom(32)).decode('utf-8')
67 | check_repo_exists('drdoctr/doctr{}'.format(rand_hex), service='travis')
68 |
69 | def test_GIT_URL():
70 | for url in [
71 | 'https://github.com/drdoctr/doctr.git',
72 | 'https://github.com/drdoctr/doctr',
73 | 'git://github.com/drdoctr/doctr.git',
74 | 'git://github.com/drdoctr/doctr',
75 | 'git@github.com:drdoctr/doctr.git',
76 | 'git@github.com:drdoctr/doctr',
77 | ]:
78 | assert GIT_URL.fullmatch(url).groups() == ('drdoctr/doctr',), url
79 |
80 | assert not GIT_URL.fullmatch('https://gitlab.com/drdoctr/doctr.git')
81 |
82 | @pytest.mark.skipif(os.environ.get('TRAVIS_REPO_SLUG', '') != 'drdoctr/doctr', reason="Not run on Travis fork builds")
83 | @pytest.mark.skipif(not on_travis(), reason="Not on Travis")
84 | def test_guess_github_repo():
85 | """
86 | Only works if run in this repo, and if cloned from origin. For safety,
87 | only run on Travis and not run on fork builds.
88 | """
89 | assert guess_github_repo() == 'drdoctr/doctr'
90 |
--------------------------------------------------------------------------------
/doctr/tests/test_travis.py:
--------------------------------------------------------------------------------
1 | """
2 | So far, very little is actually tested here, because there aren't many
3 | functions that can be tested outside of actually running them on Travis.
4 | """
5 |
6 | import tempfile
7 | import os
8 | from os.path import join
9 |
10 | import pytest
11 |
12 | from ..travis import sync_from_log, determine_push_rights, copy_to_tmp
13 |
14 | @pytest.mark.parametrize("src", ["src"])
15 | @pytest.mark.parametrize("dst", ['.', 'dst'])
16 | def test_sync_from_log(src, dst):
17 | with tempfile.TemporaryDirectory() as dir:
18 | try:
19 | old_curdir = os.path.abspath(os.curdir)
20 | os.chdir(dir)
21 |
22 | # Set up a src directory with some files
23 | os.makedirs(src)
24 |
25 | with open(join(src, 'test1'), 'w') as f:
26 | f.write('test1')
27 |
28 | os.makedirs(join(src, 'testdir'))
29 | with open(join(src, 'testdir', 'test2'), 'w') as f:
30 | f.write('test2')
31 |
32 | # Test that the sync happens
33 | added, removed = sync_from_log(src, dst, 'logfile')
34 |
35 | assert added == [
36 | join(dst, 'test1'),
37 | join(dst, 'testdir', 'test2'),
38 | 'logfile',
39 | ]
40 |
41 | assert removed == []
42 |
43 | with open(join(dst, 'test1')) as f:
44 | assert f.read() == 'test1'
45 |
46 | with open(join(dst, 'testdir', 'test2')) as f:
47 | assert f.read() == 'test2'
48 |
49 | with open('logfile') as f:
50 | assert f.read() == '\n'.join([
51 | join(dst, 'test1'),
52 | join(dst, 'testdir', 'test2'),
53 | ])
54 |
55 | # Create a new file
56 | with open(join(src, 'test3'), 'w') as f:
57 | f.write('test3')
58 |
59 | # First test it is ignored when excluded
60 | added, removed = sync_from_log(src, dst, 'logfile',
61 | exclude=['test3'])
62 |
63 | assert added == [
64 | join(dst, 'test1'),
65 | join(dst, 'testdir', 'test2'),
66 | 'logfile',
67 | ]
68 |
69 |
70 | assert removed == []
71 |
72 | with open(join(dst, 'test1')) as f:
73 | assert f.read() == 'test1'
74 |
75 | with open(join(dst, 'testdir', 'test2')) as f:
76 | assert f.read() == 'test2'
77 |
78 | assert not os.path.exists(join(dst, 'test3'))
79 |
80 | with open('logfile') as f:
81 | assert f.read() == '\n'.join([
82 | join(dst, 'test1'),
83 | join(dst, 'testdir', 'test2'),
84 | ])
85 |
86 | # Now test it it added normally
87 | added, removed = sync_from_log(src, dst, 'logfile')
88 |
89 | assert added == [
90 | join(dst, 'test1'),
91 | join(dst, 'test3'),
92 | join(dst, 'testdir', 'test2'),
93 | 'logfile',
94 | ]
95 |
96 | assert removed == []
97 |
98 | with open(join(dst, 'test1')) as f:
99 | assert f.read() == 'test1'
100 |
101 | with open(join(dst, 'testdir', 'test2')) as f:
102 | assert f.read() == 'test2'
103 |
104 | with open(join(dst, 'test3')) as f:
105 | assert f.read() == 'test3'
106 |
107 | with open('logfile') as f:
108 | assert f.read() == '\n'.join([
109 | join(dst, 'test1'),
110 | join(dst, 'test3'),
111 | join(dst, 'testdir', 'test2'),
112 | ])
113 |
114 | # Delete a file
115 | os.remove(join(src, 'test3'))
116 |
117 | # First test it is ignored with exclude
118 | added, removed = sync_from_log(src, dst, 'logfile', exclude=['test3'])
119 | assert added == [
120 | join(dst, 'test1'),
121 | join(dst, 'testdir', 'test2'),
122 | 'logfile',
123 | ]
124 |
125 | assert removed == []
126 |
127 | with open(join(dst, 'test1')) as f:
128 | assert f.read() == 'test1'
129 |
130 | with open(join(dst, 'testdir', 'test2')) as f:
131 | assert f.read() == 'test2'
132 |
133 | with open(join(dst, 'test3')) as f:
134 | assert f.read() == 'test3'
135 |
136 | with open('logfile') as f:
137 | assert f.read() == '\n'.join([
138 | join(dst, 'test1'),
139 | join(dst, 'testdir', 'test2'),
140 | ])
141 |
142 | # Then test it is removed normally
143 | # (readd it to the log file, since the exclude removed it)
144 | with open('logfile', 'a') as f:
145 | f.write('\n' + join(dst, 'test3'))
146 |
147 | added, removed = sync_from_log(src, dst, 'logfile')
148 |
149 | assert added == [
150 | join(dst, 'test1'),
151 | join(dst, 'testdir', 'test2'),
152 | 'logfile',
153 | ]
154 |
155 | assert removed == [
156 | join(dst, 'test3'),
157 | ]
158 |
159 | with open(join(dst, 'test1')) as f:
160 | assert f.read() == 'test1'
161 |
162 | with open(join(dst, 'testdir', 'test2')) as f:
163 | assert f.read() == 'test2'
164 |
165 | assert not os.path.exists(join(dst, 'test3'))
166 |
167 | with open('logfile') as f:
168 | assert f.read() == '\n'.join([
169 | join(dst, 'test1'),
170 | join(dst, 'testdir', 'test2'),
171 | ])
172 |
173 | # Change a file
174 | with open(join(src, 'test1'), 'w') as f:
175 | f.write('test1 modified')
176 |
177 | added, removed = sync_from_log(src, dst, 'logfile')
178 |
179 | assert added == [
180 | join(dst, 'test1'),
181 | join(dst, 'testdir', 'test2'),
182 | 'logfile',
183 | ]
184 |
185 | assert removed == []
186 |
187 | with open(join(dst, 'test1')) as f:
188 | assert f.read() == 'test1 modified'
189 |
190 | with open(join(dst, 'testdir', 'test2')) as f:
191 | assert f.read() == 'test2'
192 |
193 | assert not os.path.exists(join(dst, 'test3'))
194 |
195 | with open('logfile') as f:
196 | assert f.read() == '\n'.join([
197 | join(dst, 'test1'),
198 | join(dst, 'testdir', 'test2'),
199 | ])
200 |
201 | # Test excluding a directory
202 |
203 | os.makedirs(join(src, 'testdir2'))
204 | with open(join(src, 'testdir2', 'test2'), 'w') as f:
205 | f.write('test2')
206 |
207 | added, removed = sync_from_log(src, dst, 'logfile', exclude=['testdir2'])
208 |
209 |
210 | assert added == [
211 | join(dst, 'test1'),
212 | join(dst, 'testdir', 'test2'),
213 | 'logfile',
214 | ]
215 |
216 | assert removed == []
217 |
218 | assert not os.path.exists(join(dst, 'testdir2'))
219 |
220 | finally:
221 | os.chdir(old_curdir)
222 |
223 |
224 | @pytest.mark.parametrize("dst", ['dst', 'dst/'])
225 | def test_sync_from_log_file_to_dir(dst):
226 | with tempfile.TemporaryDirectory() as dir:
227 | try:
228 | old_curdir = os.path.abspath(os.curdir)
229 | os.chdir(dir)
230 |
231 | src = 'file'
232 |
233 | with open(src, 'w') as f:
234 | f.write('test1')
235 |
236 | # Test that the sync happens
237 | added, removed = sync_from_log(src, dst, 'logfile')
238 |
239 | assert added == [
240 | os.path.join('dst', 'file'),
241 | 'logfile',
242 | ]
243 |
244 | assert removed == []
245 |
246 | assert os.path.isdir(dst)
247 | # Make sure dst is a file
248 | with open(os.path.join('dst', 'file')) as f:
249 | assert f.read() == 'test1'
250 |
251 |
252 | with open('logfile') as f:
253 | assert f.read() == '\n'.join([
254 | os.path.join('dst', 'file')
255 | ])
256 |
257 | finally:
258 | os.chdir(old_curdir)
259 |
260 |
261 | @pytest.mark.parametrize("""branch_whitelist, TRAVIS_BRANCH,
262 | TRAVIS_PULL_REQUEST, TRAVIS_TAG, fork, build_tags,
263 | canpush""",
264 | [
265 |
266 | ('master', 'doctr', 'true', "", False, False, False),
267 | ('master', 'doctr', 'false', "", False, False, False),
268 | ('master', 'master', 'true', "", False, False, False),
269 | ('master', 'master', 'false', "", False, False, True),
270 | ('doctr', 'doctr', 'True', "", False, False, False),
271 | ('doctr', 'doctr', 'false', "", False, False, True),
272 | ('set()', 'doctr', 'false', "", False, False, False),
273 |
274 | ('master', 'doctr', 'true', "tagname", False, False, False),
275 | ('master', 'doctr', 'false', "tagname", False, False, False),
276 | ('master', 'master', 'true', "tagname", False, False, False),
277 | ('master', 'master', 'false', "tagname", False, False, False),
278 | ('doctr', 'doctr', 'True', "tagname", False, False, False),
279 | ('doctr', 'doctr', 'false', "tagname", False, False, False),
280 | ('set()', 'doctr', 'false', "tagname", False, False, False),
281 |
282 | ('master', 'doctr', 'true', "", False, True, False),
283 | ('master', 'doctr', 'false', "", False, True, False),
284 | ('master', 'master', 'true', "", False, True, False),
285 | ('master', 'master', 'false', "", False, True, True),
286 | ('doctr', 'doctr', 'True', "", False, True, False),
287 | ('doctr', 'doctr', 'false', "", False, True, True),
288 | ('set()', 'doctr', 'false', "", False, True, False),
289 |
290 | ('master', 'doctr', 'true', "tagname", False, True, True),
291 | ('master', 'doctr', 'false', "tagname", False, True, True),
292 | ('master', 'master', 'true', "tagname", False, True, True),
293 | ('master', 'master', 'false', "tagname", False, True, True),
294 | ('doctr', 'doctr', 'True', "tagname", False, True, True),
295 | ('doctr', 'doctr', 'false', "tagname", False, True, True),
296 | ('set()', 'doctr', 'false', "tagname", False, True, True),
297 |
298 | ('master', 'doctr', 'true', "", True, False, False),
299 | ('master', 'doctr', 'false', "", True, False, False),
300 | ('master', 'master', 'true', "", True, False, False),
301 | ('master', 'master', 'false', "", True, False, False),
302 | ('doctr', 'doctr', 'True', "", True, False, False),
303 | ('doctr', 'doctr', 'false', "", True, False, False),
304 | ('set()', 'doctr', 'false', "", True, False, False),
305 |
306 | ])
307 | def test_determine_push_rights(branch_whitelist, TRAVIS_BRANCH,
308 | TRAVIS_PULL_REQUEST, TRAVIS_TAG, build_tags, fork, canpush, monkeypatch):
309 | branch_whitelist = {branch_whitelist}
310 |
311 | assert determine_push_rights(
312 | branch_whitelist=branch_whitelist,
313 | TRAVIS_BRANCH=TRAVIS_BRANCH,
314 | TRAVIS_PULL_REQUEST=TRAVIS_PULL_REQUEST,
315 | TRAVIS_TAG=TRAVIS_TAG,
316 | fork=fork,
317 | build_tags=build_tags) == canpush
318 |
319 | @pytest.mark.parametrize("src", ["src", "."])
320 | def test_copy_to_tmp(src):
321 | with tempfile.TemporaryDirectory() as dir:
322 | os.makedirs(os.path.join(dir, src), exist_ok=True)
323 | with open(os.path.join(dir, src, "test"), 'w') as f:
324 | f.write('test')
325 |
326 | new_dir = copy_to_tmp(os.path.join(dir, src))
327 |
328 | assert os.path.exists(new_dir)
329 | with open(os.path.join(new_dir, 'test'), 'r') as f:
330 | assert f.read() == 'test'
331 |
332 | new_dir2 = copy_to_tmp(os.path.join(dir, src, 'test'))
333 |
334 | assert os.path.exists(new_dir2)
335 | with open(os.path.join(new_dir2), 'r') as f:
336 | assert f.read() == 'test'
337 |
--------------------------------------------------------------------------------
/doctr/travis.py:
--------------------------------------------------------------------------------
1 | """
2 | The code that should be run on Travis
3 | """
4 |
5 | import os
6 | import shlex
7 | import shutil
8 | import subprocess
9 | import sys
10 | import glob
11 | import re
12 | import pathlib
13 | import tempfile
14 | import time
15 |
16 | import requests
17 |
18 | from cryptography.fernet import Fernet
19 |
20 | from .common import red, blue, yellow
21 | DOCTR_WORKING_BRANCH = '__doctr_working_branch'
22 |
23 | def decrypt_file(file, key):
24 | """
25 | Decrypts the file ``file``.
26 |
27 | The encrypted file is assumed to end with the ``.enc`` extension. The
28 | decrypted file is saved to the same location without the ``.enc``
29 | extension.
30 |
31 | The permissions on the decrypted file are automatically set to 0o600.
32 |
33 | See also :func:`doctr.local.encrypt_file`.
34 |
35 | """
36 | if not file.endswith('.enc'):
37 | raise ValueError("%s does not end with .enc" % file)
38 |
39 | fer = Fernet(key)
40 |
41 | with open(file, 'rb') as f:
42 | decrypted_file = fer.decrypt(f.read())
43 |
44 | with open(file[:-4], 'wb') as f:
45 | f.write(decrypted_file)
46 |
47 | os.chmod(file[:-4], 0o600)
48 |
49 | def setup_deploy_key(keypath='github_deploy_key', key_ext='.enc', env_name='DOCTR_DEPLOY_ENCRYPTION_KEY'):
50 | """
51 | Decrypts the deploy key and configures it with ssh
52 |
53 | The key is assumed to be encrypted as keypath + key_ext, and the
54 | encryption key is assumed to be set in the environment variable
55 | ``env_name``. If ``env_name`` is not set, it falls back to
56 | ``DOCTR_DEPLOY_ENCRYPTION_KEY`` for backwards compatibility.
57 |
58 | If keypath + key_ext does not exist, it falls back to
59 | ``github_deploy_key.enc`` for backwards compatibility.
60 | """
61 | key = os.environ.get(env_name, os.environ.get("DOCTR_DEPLOY_ENCRYPTION_KEY", None))
62 | if not key:
63 | raise RuntimeError("{env_name} or DOCTR_DEPLOY_ENCRYPTION_KEY environment variable is not set. Make sure you followed the instructions from 'doctr configure' properly. You may need to re-run 'doctr configure' to fix this error."
64 | .format(env_name=env_name))
65 |
66 | # Legacy keyfile name
67 | if (not os.path.isfile(keypath + key_ext) and
68 | os.path.isfile('github_deploy_key' + key_ext)):
69 | keypath = 'github_deploy_key'
70 | key_filename = os.path.basename(keypath)
71 | key = key.encode('utf-8')
72 | decrypt_file(keypath + key_ext, key)
73 |
74 | key_path = os.path.expanduser("~/.ssh/" + key_filename)
75 | os.makedirs(os.path.expanduser("~/.ssh"), exist_ok=True)
76 | os.rename(keypath, key_path)
77 |
78 | with open(os.path.expanduser("~/.ssh/config"), 'a') as f:
79 | f.write("Host github.com"
80 | ' IdentityFile "%s"'
81 | " LogLevel ERROR\n" % key_path)
82 |
83 | # start ssh-agent and add key to it
84 | # info from SSH agent has to be put into the environment
85 | agent_info = subprocess.check_output(['ssh-agent', '-s'])
86 | agent_info = agent_info.decode('utf-8')
87 | agent_info = agent_info.split()
88 |
89 | AUTH_SOCK = agent_info[0].split('=')[1][:-1]
90 | AGENT_PID = agent_info[3].split('=')[1][:-1]
91 |
92 | os.putenv('SSH_AUTH_SOCK', AUTH_SOCK)
93 | os.putenv('SSH_AGENT_PID', AGENT_PID)
94 |
95 | run(['ssh-add', os.path.expanduser('~/.ssh/' + key_filename)])
96 |
97 | def run_command_hiding_token(args, token, shell=False):
98 | if token:
99 | stdout = stderr = subprocess.PIPE
100 | else:
101 | stdout = stderr = None
102 | p = subprocess.run(args, stdout=stdout, stderr=stderr, shell=shell)
103 | if token:
104 | # XXX: Do this in a way that is streaming
105 | out, err = p.stdout, p.stderr
106 | out = out.replace(token, b"~"*len(token))
107 | err = err.replace(token, b"~"*len(token))
108 | if out:
109 | print(out.decode('utf-8'))
110 | if err:
111 | print(err.decode('utf-8'), file=sys.stderr)
112 | return p.returncode
113 |
114 | def get_token():
115 | """
116 | Get the encrypted GitHub token in Travis.
117 |
118 | Make sure the contents this variable do not leak. The ``run()`` function
119 | will remove this from the output, so always use it.
120 | """
121 | token = os.environ.get("GH_TOKEN", None)
122 | if not token:
123 | token = "GH_TOKEN environment variable not set"
124 | token = token.encode('utf-8')
125 | return token
126 |
127 | def run(args, shell=False, exit=True):
128 | """
129 | Run the command ``args``.
130 |
131 | Automatically hides the secret GitHub token from the output.
132 |
133 | If shell=False (recommended for most commands), args should be a list of
134 | strings. If shell=True, args should be a string of the command to run.
135 |
136 | If exit=True, it exits on nonzero returncode. Otherwise it returns the
137 | returncode.
138 | """
139 | if "GH_TOKEN" in os.environ:
140 | token = get_token()
141 | else:
142 | token = b''
143 |
144 | if not shell:
145 | command = ' '.join(map(shlex.quote, args))
146 | else:
147 | command = args
148 | command = command.replace(token.decode('utf-8'), '~'*len(token))
149 | print(blue(command))
150 | sys.stdout.flush()
151 |
152 | returncode = run_command_hiding_token(args, token, shell=shell)
153 |
154 | if exit and returncode != 0:
155 | sys.exit(red("%s failed: %s" % (command, returncode)))
156 | return returncode
157 |
158 | def get_current_repo():
159 | """
160 | Get the GitHub repo name for the current directory.
161 |
162 | Assumes that the repo is in the ``origin`` remote.
163 | """
164 | remote_url = subprocess.check_output(['git', 'config', '--get',
165 | 'remote.origin.url']).decode('utf-8')
166 |
167 | # Travis uses the https clone url
168 | _, org, git_repo = remote_url.rsplit('.git', 1)[0].rsplit('/', 2)
169 | return (org + '/' + git_repo)
170 |
171 | def get_travis_branch():
172 | """Get the name of the branch that the PR is from.
173 |
174 | Note that this is not simply ``$TRAVIS_BRANCH``. the ``push`` build will
175 | use the correct branch (the branch that the PR is from) but the ``pr``
176 | build will use the _target_ of the PR (usually master). So instead, we ask
177 | for ``$TRAVIS_PULL_REQUEST_BRANCH`` if it's a PR build, and
178 | ``$TRAVIS_BRANCH`` if it's a push build.
179 | """
180 | if os.environ.get("TRAVIS_PULL_REQUEST", "") == "true":
181 | return os.environ.get("TRAVIS_PULL_REQUEST_BRANCH", "")
182 | else:
183 | return os.environ.get("TRAVIS_BRANCH", "")
184 |
185 | def setup_GitHub_push(deploy_repo, *, auth_type='deploy_key',
186 | full_key_path='github_deploy_key.enc', require_master=None,
187 | branch_whitelist=None, deploy_branch='gh-pages',
188 | env_name='DOCTR_DEPLOY_ENCRYPTION_KEY', build_tags=False):
189 | """
190 | Setup the remote to push to GitHub (to be run on Travis).
191 |
192 | ``auth_type`` should be either ``'deploy_key'`` or ``'token'``.
193 |
194 | For ``auth_type='token'``, this sets up the remote with the token and
195 | checks out the gh-pages branch. The token to push to GitHub is assumed to be in the ``GH_TOKEN`` environment
196 | variable.
197 |
198 | For ``auth_type='deploy_key'``, this sets up the remote with ssh access.
199 | """
200 | # Set to the name of the tag for tag builds
201 | TRAVIS_TAG = os.environ.get("TRAVIS_TAG", "")
202 |
203 | if branch_whitelist is None:
204 | branch_whitelist={'master'}
205 |
206 | if require_master is not None:
207 | import warnings
208 | warnings.warn("`setup_GitHub_push`'s `require_master` argument in favor of `branch_whitelist=['master']`",
209 | DeprecationWarning,
210 | stacklevel=2)
211 | branch_whitelist.add('master')
212 |
213 | if auth_type not in ['deploy_key', 'token']:
214 | raise ValueError("auth_type must be 'deploy_key' or 'token'")
215 |
216 | TRAVIS_BRANCH = os.environ.get("TRAVIS_BRANCH", "")
217 | TRAVIS_PULL_REQUEST = os.environ.get("TRAVIS_PULL_REQUEST", "")
218 |
219 | # Check if the repo is a fork
220 | TRAVIS_REPO_SLUG = os.environ["TRAVIS_REPO_SLUG"]
221 | REPO_URL = 'https://api.github.com/repos/{slug}'
222 | r = requests.get(REPO_URL.format(slug=TRAVIS_REPO_SLUG))
223 | fork = r.json().get('fork', False)
224 |
225 | canpush = determine_push_rights(
226 | branch_whitelist=branch_whitelist,
227 | TRAVIS_BRANCH=TRAVIS_BRANCH,
228 | TRAVIS_PULL_REQUEST=TRAVIS_PULL_REQUEST,
229 | fork=fork,
230 | TRAVIS_TAG=TRAVIS_TAG,
231 | build_tags=build_tags)
232 |
233 | print("Setting git attributes")
234 | set_git_user_email()
235 |
236 | remotes = subprocess.check_output(['git', 'remote']).decode('utf-8').split('\n')
237 | if 'doctr_remote' in remotes:
238 | print("doctr_remote already exists, removing")
239 | run(['git', 'remote', 'remove', 'doctr_remote'])
240 | print("Adding doctr remote")
241 | if canpush:
242 | if auth_type == 'token':
243 | token = get_token()
244 | run(['git', 'remote', 'add', 'doctr_remote',
245 | 'https://{token}@github.com/{deploy_repo}.git'.format(token=token.decode('utf-8'),
246 | deploy_repo=deploy_repo)])
247 | else:
248 | keypath, key_ext = full_key_path.rsplit('.', 1)
249 | key_ext = '.' + key_ext
250 | try:
251 | setup_deploy_key(keypath=keypath, key_ext=key_ext, env_name=env_name)
252 | except RuntimeError:
253 | # Rate limits prevent this check from working every time. By default, we
254 | # assume it isn't a fork so that things just work on non-fork builds.
255 | if r.status_code == 403:
256 | print(yellow("Warning: GitHub's API rate limits prevented doctr from detecting if this build is a forked repo. If it is, you may ignore the 'DOCTR_DEPLOY_ENCRYPTION_KEY environment variable is not set' error that follows. If it is not, you should re-run 'doctr configure'. Note that doctr cannot deploy from fork builds due to limitations in Travis."), file=sys.stderr)
257 | raise
258 |
259 | run(['git', 'remote', 'add', 'doctr_remote',
260 | 'git@github.com:{deploy_repo}.git'.format(deploy_repo=deploy_repo)])
261 | else:
262 | print('setting a read-only GitHub doctr_remote')
263 | run(['git', 'remote', 'add', 'doctr_remote',
264 | 'https://github.com/{deploy_repo}.git'.format(deploy_repo=deploy_repo)])
265 |
266 |
267 | print("Fetching doctr remote")
268 | run(['git', 'fetch', 'doctr_remote'])
269 |
270 | return canpush
271 |
272 | def set_git_user_email():
273 | """
274 | Set global user and email for git user if not already present on system
275 | """
276 | username = subprocess.run(shlex.split('git config user.name'), stdout=subprocess.PIPE).stdout.strip().decode('utf-8')
277 | if not username or username == "Travis CI User":
278 | run(['git', 'config', '--global', 'user.name', "Doctr (Travis CI)"])
279 | else:
280 | print("Not setting git user name, as it's already set to %r" % username)
281 |
282 | email = subprocess.run(shlex.split('git config user.email'), stdout=subprocess.PIPE).stdout.strip().decode('utf-8')
283 | if not email or email == "travis@example.org":
284 | # We need a dummy email or git will fail. We use this one as per
285 | # https://help.github.com/articles/keeping-your-email-address-private/.
286 | run(['git', 'config', '--global', 'user.email', 'drdoctr@users.noreply.github.com'])
287 | else:
288 | print("Not setting git user email, as it's already set to %r" % email)
289 |
290 | def checkout_deploy_branch(deploy_branch, canpush=True):
291 | """
292 | Checkout the deploy branch, creating it if it doesn't exist.
293 | """
294 | # Create an empty branch with .nojekyll if it doesn't already exist
295 | create_deploy_branch(deploy_branch, push=canpush)
296 | remote_branch = "doctr_remote/{}".format(deploy_branch)
297 | print("Checking out doctr working branch tracking", remote_branch)
298 | clear_working_branch()
299 | # If gh-pages doesn't exist the above create_deploy_branch() will create
300 | # it we can push, but if we can't, it won't and the --track would fail.
301 | if run(['git', 'rev-parse', '--verify', remote_branch], exit=False) == 0:
302 | extra_args = ['--track', remote_branch]
303 | else:
304 | extra_args = []
305 | run(['git', 'checkout', '-b', DOCTR_WORKING_BRANCH] + extra_args)
306 | print("Done")
307 |
308 | return canpush
309 |
310 | def clear_working_branch():
311 | local_branch_names = subprocess.check_output(['git', 'branch']).decode('utf-8').split()
312 | if DOCTR_WORKING_BRANCH in local_branch_names:
313 | run(['git', 'branch', '-D', DOCTR_WORKING_BRANCH])
314 |
315 | def deploy_branch_exists(deploy_branch):
316 | """
317 | Check if there is a remote branch with name specified in ``deploy_branch``.
318 |
319 | Note that default ``deploy_branch`` is ``gh-pages`` for regular repos and
320 | ``master`` for ``github.io`` repos.
321 |
322 | This isn't completely robust. If there are multiple remotes and you have a
323 | ``deploy_branch`` branch on the non-default remote, this won't see it.
324 | """
325 | remote_name = 'doctr_remote'
326 | branch_names = subprocess.check_output(['git', 'branch', '-r']).decode('utf-8').split()
327 |
328 | return '{}/{}'.format(remote_name, deploy_branch) in branch_names
329 |
330 | def create_deploy_branch(deploy_branch, push=True):
331 | """
332 | If there is no remote branch with name specified in ``deploy_branch``,
333 | create one.
334 |
335 | Note that default ``deploy_branch`` is ``gh-pages`` for regular
336 | repos and ``master`` for ``github.io`` repos.
337 |
338 | Return True if ``deploy_branch`` was created, False if not.
339 | """
340 | if not deploy_branch_exists(deploy_branch):
341 | print("Creating {} branch on doctr_remote".format(deploy_branch))
342 | clear_working_branch()
343 | run(['git', 'checkout', '--orphan', DOCTR_WORKING_BRANCH])
344 | # delete everything in the new ref. this is non-destructive to existing
345 | # refs/branches, etc...
346 | run(['git', 'rm', '-rf', '.'])
347 | print("Adding .nojekyll file to working branch")
348 | run(['touch', '.nojekyll'])
349 | run(['git', 'add', '.nojekyll'])
350 | run(['git', 'commit', '-m', 'Create new {} branch with .nojekyll'.format(deploy_branch)])
351 | if push:
352 | print("Pushing working branch to remote {} branch".format(deploy_branch))
353 | run(['git', 'push', '-u', 'doctr_remote', '{}:{}'.format(DOCTR_WORKING_BRANCH, deploy_branch)])
354 | # return to master branch and clear the working branch
355 | run(['git', 'checkout', 'master'])
356 | run(['git', 'branch', '-D', DOCTR_WORKING_BRANCH])
357 | # fetch the remote so that doctr_remote/{deploy_branch} is resolved
358 | run(['git', 'fetch', 'doctr_remote'])
359 |
360 | return True
361 | return False
362 |
363 | def find_sphinx_build_dir():
364 | """
365 | Find build subfolder within sphinx docs directory.
366 |
367 | This is called by :func:`commit_docs` if keyword arg ``built_docs`` is not
368 | specified on the command line.
369 | """
370 | build = glob.glob('**/*build/html', recursive=True)
371 | if not build:
372 | raise RuntimeError("Could not find Sphinx build directory automatically")
373 | build_folder = build[0]
374 |
375 | return build_folder
376 |
377 | # Here is the logic to get the Travis job number, to only run commit_docs in
378 | # the right build.
379 | #
380 | # TRAVIS_JOB_NUMBER = os.environ.get("TRAVIS_JOB_NUMBER", '')
381 | # ACTUAL_TRAVIS_JOB_NUMBER = TRAVIS_JOB_NUMBER.split('.')[1]
382 |
383 | def copy_to_tmp(source):
384 | """
385 | Copies ``source`` to a temporary directory, and returns the copied
386 | location.
387 |
388 | If source is a file, the copied location is also a file.
389 | """
390 | tmp_dir = tempfile.mkdtemp()
391 | # Use pathlib because os.path.basename is different depending on whether
392 | # the path ends in a /
393 | p = pathlib.Path(source)
394 | dirname = p.name or 'temp'
395 | new_dir = os.path.join(tmp_dir, dirname)
396 | if os.path.isdir(source):
397 | shutil.copytree(source, new_dir)
398 | else:
399 | shutil.copy2(source, new_dir)
400 | return new_dir
401 |
402 | def is_subdir(a, b):
403 | """
404 | Return true if a is a subdirectory of b
405 | """
406 | a, b = map(os.path.abspath, [a, b])
407 |
408 | return os.path.commonpath([a, b]) == b
409 |
410 | def sync_from_log(src, dst, log_file, exclude=()):
411 | """
412 | Sync the files in ``src`` to ``dst``.
413 |
414 | The files that are synced are logged to ``log_file``. If ``log_file``
415 | exists, the files in ``log_file`` are removed first.
416 |
417 | Returns ``(added, removed)``, where added is a list of all files synced from
418 | ``src`` (even if it already existed in ``dst``), and ``removed`` is every
419 | file from ``log_file`` that was removed from ``dst`` because it wasn't in
420 | ``src``. ``added`` also includes the log file.
421 |
422 | ``exclude`` may be a list of paths from ``src`` that should be ignored.
423 | Such paths are neither added nor removed, even if they are in the logfile.
424 | """
425 | from os.path import join, exists, isdir
426 |
427 | exclude = [os.path.normpath(i) for i in exclude]
428 |
429 | added, removed = [], []
430 |
431 | if not exists(log_file):
432 | # Assume this is the first run
433 | print("%s doesn't exist. Not removing any files." % log_file)
434 | else:
435 | with open(log_file) as f:
436 | files = f.read().strip().split('\n')
437 |
438 | for new_f in files:
439 | new_f = new_f.strip()
440 | if any(is_subdir(new_f, os.path.join(dst, i)) for i in exclude):
441 | pass
442 | elif exists(new_f):
443 | os.remove(new_f)
444 | removed.append(new_f)
445 | else:
446 | print("Warning: File %s doesn't exist." % new_f, file=sys.stderr)
447 |
448 | if os.path.isdir(src):
449 | if not src.endswith(os.sep):
450 | src += os.sep
451 | files = glob.iglob(join(src, '**'), recursive=True)
452 | else:
453 | files = [src]
454 | src = os.path.dirname(src) + os.sep if os.sep in src else ''
455 |
456 | os.makedirs(dst, exist_ok=True)
457 |
458 | # sorted makes this easier to test
459 | for f in sorted(files):
460 | if any(is_subdir(f, os.path.join(src, i)) for i in exclude):
461 | continue
462 | new_f = join(dst, f[len(src):])
463 |
464 | if isdir(f) or f.endswith(os.sep):
465 | os.makedirs(new_f, exist_ok=True)
466 | else:
467 | shutil.copy2(f, new_f)
468 | added.append(new_f)
469 | if new_f in removed:
470 | removed.remove(new_f)
471 |
472 | with open(log_file, 'w') as f:
473 | f.write('\n'.join(added))
474 |
475 | added.append(log_file)
476 |
477 | return added, removed
478 |
479 | def commit_docs(*, added, removed):
480 | """
481 | Commit the docs to the current branch
482 |
483 | Assumes that :func:`setup_GitHub_push`, which sets up the ``doctr_remote``
484 | remote, has been run.
485 |
486 | Returns True if changes were committed and False if no changes were
487 | committed.
488 | """
489 | TRAVIS_BUILD_NUMBER = os.environ.get("TRAVIS_BUILD_NUMBER", "")
490 | TRAVIS_BRANCH = os.environ.get("TRAVIS_BRANCH", "")
491 | TRAVIS_COMMIT = os.environ.get("TRAVIS_COMMIT", "")
492 | TRAVIS_REPO_SLUG = os.environ.get("TRAVIS_REPO_SLUG", "")
493 | TRAVIS_JOB_WEB_URL = os.environ.get("TRAVIS_JOB_WEB_URL", "")
494 | TRAVIS_TAG = os.environ.get("TRAVIS_TAG", "")
495 | branch = "tag" if TRAVIS_TAG else "branch"
496 |
497 | DOCTR_COMMAND = ' '.join(map(shlex.quote, sys.argv))
498 |
499 |
500 | if added:
501 | run(['git', 'add', *added])
502 | if removed:
503 | run(['git', 'rm', *removed])
504 |
505 | commit_message = """\
506 | Update docs after building Travis build {TRAVIS_BUILD_NUMBER} of
507 | {TRAVIS_REPO_SLUG}
508 |
509 | The docs were built from the {branch} '{TRAVIS_BRANCH}' against the commit
510 | {TRAVIS_COMMIT}.
511 |
512 | The Travis build that generated this commit is at
513 | {TRAVIS_JOB_WEB_URL}.
514 |
515 | The doctr command that was run is
516 |
517 | {DOCTR_COMMAND}
518 | """.format(
519 | branch=branch,
520 | TRAVIS_BUILD_NUMBER=TRAVIS_BUILD_NUMBER,
521 | TRAVIS_BRANCH=TRAVIS_BRANCH,
522 | TRAVIS_COMMIT=TRAVIS_COMMIT,
523 | TRAVIS_REPO_SLUG=TRAVIS_REPO_SLUG,
524 | TRAVIS_JOB_WEB_URL=TRAVIS_JOB_WEB_URL,
525 | DOCTR_COMMAND=DOCTR_COMMAND,
526 | )
527 |
528 | # Only commit if there were changes
529 | if run(['git', 'diff-index', '--exit-code', '--cached', '--quiet', 'HEAD', '--'], exit=False) != 0:
530 | print("Committing")
531 | run(['git', 'commit', '-am', commit_message])
532 | return True
533 |
534 | return False
535 |
536 | def push_docs(deploy_branch='gh-pages', retries=5):
537 | """
538 | Push the changes to the branch named ``deploy_branch``.
539 |
540 | Assumes that :func:`setup_GitHub_push` has been run and returned True, and
541 | that :func:`commit_docs` has been run. Does not push anything if no changes
542 | were made.
543 |
544 | """
545 |
546 | code = 1
547 | while code and retries:
548 | print("Pulling")
549 | code = run(['git', 'pull', '-s', 'recursive', '-X', 'ours',
550 | 'doctr_remote', deploy_branch], exit=False)
551 | print("Pushing commit")
552 | code = run(['git', 'push', '-q', 'doctr_remote',
553 | '{}:{}'.format(DOCTR_WORKING_BRANCH, deploy_branch)], exit=False)
554 | if code:
555 | retries -= 1
556 | print("Push failed, retrying")
557 | time.sleep(1)
558 | else:
559 | return
560 | sys.exit("Giving up...")
561 |
562 | def last_commit_by_doctr():
563 | """Check whether the author of `HEAD` is `doctr` to avoid starting an
564 | infinite loop"""
565 |
566 | email = subprocess.check_output(["git", "show", "-s", "--format=%ae", "HEAD"]).decode('utf-8')
567 | if email.strip() == "drdoctr@users.noreply.github.com":
568 | return True
569 | return False
570 |
571 | def determine_push_rights(*, branch_whitelist, TRAVIS_BRANCH,
572 | TRAVIS_PULL_REQUEST, TRAVIS_TAG, build_tags, fork):
573 | """Check if Travis is running on ``master`` (or a whitelisted branch) to
574 | determine if we can/should push the docs to the deploy repo
575 | """
576 | canpush = True
577 |
578 | if TRAVIS_TAG:
579 | if not build_tags:
580 | print("The docs are not pushed on tag builds. To push on future tag builds, use --build-tags")
581 | return build_tags
582 |
583 | if not any([re.compile(x).match(TRAVIS_BRANCH) for x in branch_whitelist]):
584 | print("The docs are only pushed to gh-pages from master. To allow pushing from "
585 | "a non-master branch, use the --no-require-master flag", file=sys.stderr)
586 | print("This is the {TRAVIS_BRANCH} branch".format(TRAVIS_BRANCH=TRAVIS_BRANCH), file=sys.stderr)
587 | canpush = False
588 |
589 | if TRAVIS_PULL_REQUEST != "false":
590 | print("The website and docs are not pushed to gh-pages on pull requests", file=sys.stderr)
591 | canpush = False
592 |
593 | if fork:
594 | print("The website and docs are not pushed to gh-pages on fork builds.", file=sys.stderr)
595 | canpush = False
596 |
597 | if last_commit_by_doctr():
598 | print(red("The last commit on this branch was pushed by doctr. Not pushing to "
599 | "avoid an infinite build-loop."), file=sys.stderr)
600 | canpush = False
601 |
602 | return canpush
603 |
--------------------------------------------------------------------------------
/github_deploy_key_drdoctr_drdoctr_github_io.enc:
--------------------------------------------------------------------------------
1 | gAAAAABaEKMzZx2NgtXxFNXgUjB0b8BcLhAxlMokLn6LKbfUZs0tZgANr99OpthSs1GKla3E0qEkhNz-XZZzpLqMYUc27xpPqi6vj634NEZKeLk35F9Ex9TgbO7CHNuWVrd8fs-bXAlgceRsbb6AUEQs8ZKA_alDFnH1Rk_HKAdFwpHtN_sxL2ZZXNK2DgHrY1b3sOe_8ZECpv0uZHHpmTyIM1uUQNjooIS4M1SUOc4kynBwY228F8a-sA3b2QRG-mZyaZt0760UxozH_xo8Tst2iSgvTtWUQaPcVMM3nKQtK0XTw1fQlaL8ylmKpqg2sJLJk1fJYX1GHRQ00IroqiBm02N1TbfB3zjKG7zO4ngQhzYiKLHIB-DpvgwvCZaP6FitgNTk50xYTR3UWZxv8yYoxFcD0AJxZyBYjGqc4v6wbkTGGclGDp6Q2OG3IUat_htu5TcT0ymx1GiYEf0SCsjiqZJG4wMfqe9jvcw8CxfbYcjhCAoOmM0Za0x9LdgMhcTftuw2qrrS6jFkjtpyLxvXtrLh0aU0WWh5A6592HscdrD6bhGYtnl488GagMdZbzKwLeF1lM0Bezq6KFYtXG0YNuuIeqn2du9OrkF8wjO4RDvbLdT-MTAkAd8Dmp2EjizEjju3hA14PEYuyYSfV-86a6NbrWg2pzjlOehacb4TNfpIokSFQiPGn6Z4JbFjcz1NXCLbNoFA3WHc8zJPIZOl_tNZIXVBHhaKy8MMqRUwwcwy5VuK3HwPq46FKy-WI-kw9EvfSt_Bk0r4UD0aaqtKwWXrf3GEdxp6lj2LZNKC6Lnj-1t4nrLDdXrzt2U8HtyCpnzzY0GG-JwEfdM_2Cq7kIQp6kDpDIo1qYXR5-BIKOfuPtMJZRob3I3bG0heOYFfefVhsb7o5DrVssSR8OSK6yDwwDnC4ew36bxnTv5m9d_rzwIHAdW5qm__H-5zbHrZRAW5vRoXZ2fUWex9p4hUgUTp6Ncc2XOMW_wNbnrTgR_khZYyheaAR6b0HA_q6e8n2JfBx9xSHQxRUVv95ECe5mX5zXJOl_ltad0L0eXHF8BpSIpVLHYzEIJKpixa-mzb-ickSkIHs2h8AgTAJhJsN6-Za9a6JRAkIi6kgIOo2Iw0gcuCYGmrRlUrEIWAiwA5K-3CraQU9zEinPODDIQe2aN8t5QMqqyZoLiyNzsDuVzmlD529sip5ADkjpIjxXZSmc8mGc4fyKqGzibdMVnBbYOifB1Yd0hLPJsjK7D8_CnINGiVFXt2aY7lB5NinvYSvBq7afd980N1Cux_B84RGpkvvhSwD14rXYA1A9vIP5_BcaIZ3BwtSrQ85MvGQLQK-LHlbP186kmCYJekEdDIn1K--jCuVdSU9rFxM-lS3TFTmRzhgomoIirgGv-ZGggGaZPKQRVUNmnDy9ahuwyvbb8pB5tTFZl_Ai3ByATIvnM_Bgz085e-62egYZ3ylzwQqDqp1RettW2RN_BhqzW8ZT5OTcnBWSHTYHjaQrwSFteTFAXAGMjoUsXE7hTIvVRfzhzNSTqU32jLeNGDFr-UzCYSdIsqQQJIlsZy6NCjSvzEqS24T_Naa8IFtkhebkXVDRbbYytpkhMKMXxZtDoBf87LDEvdF3zq2GLmuAknbiIuKX3EH_cKbNdf8ilXmZmecCOjrcKx3d-zTDMMoya-h8-Md54LMVlA1Llqhb4oP7iJtLM_X7VTQheD2fs1qlrsWQQFRzw9B3cOkdBWnAQHHBIhEvXSrYy3rInjp6lD6WdzAEDPh1H_iLj5FoEWDxL8H-8mo1K4FjonACcs497C5zJoHyv4b1ywV6BEKIrW3TzvrCRKXwCVS3wgh_f5yAQ36lpU37HaTNmVjofhMc17fNs3W73WD-E-cG-qmEeavIQ9ZYWobQ2MmNOyKgm_hsL5o3AgQUUCI3cDjveUoWHOLsZTupNR87DNNOlzH1Yl0W8_qYg5BhUWyl4o-rarsCqkcJKYZMp9A87ff46AXlzVoi63fMVks3Mc5axjvn69rmwOKdB-ieWpMdipE0XHNO2Co2XF1DLuMtLD0iV0rp5v8c7Ep6KtrGWrR2UKniz4n8BiWNXrCcG5nvkfbvBCa4BrPtqjYJDFQpjvjWIuBbQFbx6k559H8aQc6lc967RA3YV3CJz8NkEqxYR9Me0p_yGzl0bhk0ohN5e1YV-zn2bw_0p6tEng1Q9AwdyJ3aWI85noxRA56TPi9KUqCbNK_fjnJWvcfMgcwxufHEzuXK6YsTT9Xei-oSI4M8TDYASl86WWFt92k5FZUZe3H0ZNl6BSvB8UFap0u61mjyJy4qeiyDdcohfLdr5fwk3LcTi-52o-FZS59ciV-7B26wweUqLtS1dYyq8NbJkmpwySJTQLhJBQW02BX1w4_tBEZHMIrSP5j9c4aVhOzbRrlaGMkGk3DoJAzG3ksxIwFCoEcdFqcZ5YlSq0EcaMTTdN3uZqMgn0zY2JPUptu69mc4sSkSyLKqr42rYNnJKoHWsfML29z5mJCEe20itilCiuJRVHaCYuChrrvaSrq9U22gXQV7bs7oZp5hVfxHpsUbIh7PpNPaYJVrLHN7AUzdx-d2TCzxc6AJqBqtG8W8fk6JYxDZDuZHK1In0Rzh_5v12CW2yYoqpy90EVRELL3DRtqdVr21UvOSZa4DzD6M_vAtcIOsltv4HVssyXtQFpBjI2-0Ny3Qcyiasl9DKYRNQg0xICzUZbA9-PIG1D6pF6BQ3FOOQcSNkbVAMSNJJFo-tjilJtVNXc_Dyd65Wb527eyXUrcgRiO2sT06QaUXGBqOCiwLGS2PunZ2BqFVkUCWyM7AWpYtV3xxLMYyJIwWhCn3jmOq5BWJfXUy8jJKlpJR-nwJGWjImHN6Vv-Ky9PBHMPghc437efvHLCVm7pD_yW3H8YjZRDQsHUyqGyzuIEPsdXWg_QMTpQ-T5B7lAt_36sSTweqsKzECW_k3HZL-3uN5kpd72XVC3-X3qfand4Kh6GOsVclwiyNtjenEs3tc-mDbDS8y0ADSJ6ozpBR2ihpPpUt1ouelBnlyLGPOtG5ylK7a_gMlfmX1syAMJrgXK1HtCqY1KfJEBpLWRMlVGJaYFbBZBMmmApttPOC_9N-Zxh1EiM3mArEHoIxofy27lg7DvIW9866LcfSqYls4HQdzrA4wX7wznJD63vMgcA3oVsinsBGrydkflcgGPYqVl0ZWsZy7L4cCAerZpTwsTrSDRP0gpv8lh2C3ZRlXOsB1b-Cd8nGanOKwZf0XTxbt24Ip4wogD4uKeVv5SQQKWK4jXeNNAqZ5wclUgWVIoFBnpbAGLyEz1ZRNrQAb1xuaLcwPlbtllfeBoAzHI8YWTxWALNFYFzdIGSpEm_tkEGa03p2gxB4v6x8rtOugfJ8MkQJzENZ9X9BHMmQ9mKGUw6mAIq1ZhZGshDM7OzcgmmZyXBCkXaP_iLtp6bSwauKyeJo6ZRSKgyYyQiC7MeYTlWklk1Cxi_HEI6c0a0WA2I1lCWGSVxMv1ccqoPO9HAg44oNRt5EKfZaE1Qjxmk0iFaEQlhFxJyX3s2xr9uvh13B2BuH5Y_WNv9JFTNl8B92EjBaAD-4xdRqRrYM-lTCWS4VIdp8lwYywb9m64jJwafHN_N-A9DfWQohYKXaUTJeE-YL0axHQQLudV5N3kKMnMNKSqMspLFBe-BLvObQLICLL_qga2UGCMiq9lKxu4GN_I6npFLED1oNt4hVcl8oDXZ_S_E9G0Mo5mC3SLNL6ZbWSIyrTipTvirx4gU4dhGd4vCiAPyTQcRSxXUUeuYX0XaY0XRcl819CO21-Fc-XEu22_URymNw9sbTS_z-70FXE5k8XUfHy8T7TZOkV1fu4XSKW_dQaLWTPVnJuzMRso_gam_T3eNL3ofRJMUqaZrxG1x70VDM1IvvPsRAlEoh53JCePxBm26OVJ0e-3InD2nRBlDryz0J6hJs7ka72W7SHHlWwGlXPhZPIgAGoDdDGBOZUY16rkfrhcAEuWJrG7rqS7qQtZugqCEWQcfVXV1XrKaRBtWUNM88PZehQhujGWGEddYljC178eJeJSm2x1Ml81c3o87W2WEQH7QWcdFJK9YiyGkgK_vLZSjqnVxFgA5UjyAHMtqBVnK8BqlfxLd038aALH68nUgl6VsChn-TQfhbi7_uVgW_XfhTOdKnPbfYUaLNmVDoqrHULBYEydaVmEgDhxOun7SBnLH3Goz-KKsxoul2hxA5gT4xx2go9cCmtxmR5vVd6gtD8eEHsvO1gtgM3ML0rMQugH7kTQYV1IprUZ_dXRXayolspqD4heaK8jTfIEwGq7_WQm8XWg3z_YojjZ4FflC5NXgC1NoZnMAoHof-dUo9_iHzxMwM6uyJjif4zS_AjGQvI=
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 |
2 | # See the docstring in versioneer.py for instructions. Note that you must
3 | # re-run 'versioneer.py setup' after changing this section, and commit the
4 | # resulting files.
5 |
6 | [versioneer]
7 | VCS = git
8 | style = pep440
9 | versionfile_source = doctr/_version.py
10 | versionfile_build = doctr/_version.py
11 | tag_prefix =
12 | parentdir_prefix =
13 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import sys
4 | if sys.version_info < (3,5):
5 | sys.exit("doctr requires Python 3.5 or newer")
6 |
7 | from setuptools import setup
8 | import versioneer
9 |
10 | setup(
11 | name='doctr',
12 | version=versioneer.get_version(),
13 | cmdclass=versioneer.get_cmdclass(),
14 | author='Aaron Meurer and Gil Forsyth',
15 | author_email='asmeurer@gmail.com',
16 | url='https://github.com/drdoctr/doctr',
17 | packages=['doctr', 'doctr.tests'],
18 | description='Deploy docs from Travis to GitHub pages.',
19 | long_description=open("README.rst").read(),
20 | entry_points={'console_scripts': [ 'doctr = doctr.__main__:main']},
21 | python_requires= '>=3.5',
22 | install_requires=[
23 | 'pyyaml',
24 | 'requests',
25 | 'cryptography',
26 | ],
27 | license="MIT",
28 | classifiers=[
29 | 'Programming Language :: Python :: 3',
30 | 'Programming Language :: Python :: 3.5',
31 | 'Programming Language :: Python :: 3.6',
32 | 'Topic :: Documentation',
33 | 'Topic :: Software Development :: Documentation',
34 | ],
35 | zip_safe=False,
36 | )
37 |
--------------------------------------------------------------------------------
/test:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drdoctr/doctr/7e757118a1807aed5b7e95c5404bd47c6dbdccd0/test
--------------------------------------------------------------------------------