├── .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 |

{{ _('Table Of Contents') }}

14 | {{ toc }} 15 | {%- endif %} 16 | -------------------------------------------------------------------------------- /docs/_templates/sidebarhelp.html: -------------------------------------------------------------------------------- 1 |

Need help?

2 | 3 |

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 --------------------------------------------------------------------------------