├── .coveragerc ├── .dir-locals.el ├── .github └── workflows │ └── python-tests.yml ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.org ├── docs ├── README.rst ├── conf.py └── index.rst ├── elisp └── update-readme.el ├── flake.lock ├── flake.nix ├── format-code ├── generate-dependency-graph ├── make.bat ├── nix ├── dev-shell.nix ├── nix-prefetch-github.nix └── package-overrides.nix ├── nix_prefetch_github ├── VERSION ├── __init__.py ├── __main__.py ├── alerter.py ├── cli │ ├── __init__.py │ ├── fetch_directory.py │ ├── fetch_github.py │ └── fetch_latest_release.py ├── command │ ├── __init__.py │ ├── command_runner.py │ └── test_command_runner.py ├── controller │ ├── __init__.py │ ├── arguments.py │ ├── nix_prefetch_github_controller.py │ ├── nix_prefetch_github_directory_controller.py │ ├── nix_prefetch_github_latest_release_controller.py │ ├── test_arguments.py │ ├── test_nix_prefetch_github_controller.py │ ├── test_nix_prefetch_github_directory_controller.py │ └── test_nix_prefetch_github_latest_release_controller.py ├── dependency_injector.py ├── functor.py ├── github.py ├── hash.py ├── hash_converter.py ├── interfaces.py ├── list_remote.py ├── list_remote_factory.py ├── logging.py ├── prefetch.py ├── presenter │ ├── __init__.py │ ├── repository_renderer.py │ ├── test_rendering_selector_impl.py │ └── test_repository_renderer.py ├── process_environment.py ├── repository_detector.py ├── revision_index.py ├── revision_index_factory.py ├── templates.py ├── test_github.py ├── test_hash.py ├── test_hash_converter.py ├── test_list_remote.py ├── test_list_remote_factory.py ├── test_logging.py ├── test_prefetch.py ├── test_prefetch_options.py ├── test_presenter.py ├── test_repository_detector.py ├── test_revision_index.py ├── test_revision_index_factory.py ├── tests.py ├── url_hasher │ ├── __init__.py │ ├── nix_prefetch.py │ └── test_nix_prefetch_url_hasher.py ├── use_cases │ ├── __init__.py │ ├── prefetch_directory.py │ ├── prefetch_github_repository.py │ ├── prefetch_latest_release.py │ └── test_prefetch_github_repository.py ├── version.py └── views.py ├── pyproject.toml ├── release.sh ├── setup.cfg ├── setup.py ├── test-pypi-install ├── tests ├── __init__.py ├── jraygauthier_nixos_secure_factory_git_ls_remote.txt ├── sensu_go_git_ls_remote.txt ├── test_integration.py ├── test_issue_21.py └── test_issue_22.py └── update-readme /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | */test_*.py 4 | nix_prefetch_github/tests.py 5 | source = 6 | nix_prefetch_github 7 | 8 | [report] 9 | exclude_lines = 10 | pragma: no cover 11 | class .*\bProtocol\): 12 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((org-mode . ((org-confirm-babel-evaluate 2 | . 3 | (lambda (lang body) 4 | (not (string= lang "sh"))))))) 5 | -------------------------------------------------------------------------------- /.github/workflows/python-tests.yml: -------------------------------------------------------------------------------- 1 | name: nix-prefetch-github tests 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build-nix: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4.1.0 11 | - uses: cachix/install-nix-action@v24 12 | with: 13 | nix_path: nixpkgs=channel:nixos-unstable 14 | - uses: cachix/cachix-action@v12 15 | with: 16 | name: nix-prefetch-github 17 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 18 | - run: nix flake check --print-build-logs 19 | - run: nix develop --command pytest -v 20 | - run: nix develop .#stable --command pytest -v 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | /.pytest_cache 3 | __pycache__ 4 | /.coverage 5 | /coverage 6 | /dist 7 | /MANIFEST 8 | /testenv 9 | *.egg-info 10 | /result 11 | /result-* 12 | /test.nix 13 | /.mypy_cache 14 | /.envrc 15 | /nix_prefetch_github.svg 16 | /.dmypy.json 17 | /build 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: nix 2 | script: ./test-prefetch 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.org 2 | include nix_prefetch_github/VERSION 3 | include LICENSE.txt 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = docs 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+title: nix-prefetch-github 2 | 3 | * Introduction 4 | This module implements a python function and a command line tool to 5 | help you fetch sources from github when using =fetchFromGitHub=. 6 | 7 | This program can be distributed under the conditions of the GNU 8 | Public License Version 3. Check out =LICENSE.txt= to read the 9 | license text. 10 | 11 | * Dependencies 12 | - Python and its standard library 13 | - nix-prefetch-url 14 | - nix-prefech-github 15 | - git 16 | - nix 17 | 18 | * Command Line Example 19 | #+begin_src sh :results verbatim :export :wrap example :exports both 20 | result/bin/nix-prefetch-github seppeljordan nix-prefetch-github 21 | #+end_src 22 | 23 | #+RESULTS: 24 | #+begin_example 25 | { 26 | "owner": "seppeljordan", 27 | "repo": "nix-prefetch-github", 28 | "rev": "9578399cadb1cb2b252438cf14663333e8c3ee00", 29 | "hash": "sha256-JFC1+y+FMs2TwWjJxlAKAyDbSLFBE9J65myp7+slp50=" 30 | } 31 | #+end_example 32 | 33 | * Available Commands 34 | ** nix-prefetch-github 35 | This command downloads the code from a github repository and puts 36 | it into the local nix store. It also prints the function arguments 37 | to =fetchFromGitHub= to the standard output. : 38 | 39 | #+begin_src sh :results verbatim :wrap example :exports results 40 | result/bin/nix-prefetch-github --help 41 | #+end_src 42 | 43 | #+RESULTS: 44 | #+begin_example 45 | usage: nix-prefetch-github [-h] [--fetch-submodules] [--no-fetch-submodules] 46 | [--leave-dot-git] [--no-leave-dot-git] 47 | [--deep-clone] [--no-deep-clone] [--verbose] 48 | [--quiet] [--nix] [--json] [--meta] [--version] 49 | [--rev REV] 50 | owner repo 51 | 52 | positional arguments: 53 | owner 54 | repo 55 | 56 | options: 57 | -h, --help show this help message and exit 58 | --fetch-submodules Include git submodules in the output derivation 59 | --no-fetch-submodules 60 | Don't include git submodules in output derivation 61 | --leave-dot-git Include .git folder in output derivation. Use this if 62 | you need repository data, e.g. current commit hash, 63 | for the build process. 64 | --no-leave-dot-git Don't include .git folder in output derivation. 65 | --deep-clone Include all of the repository history in the output 66 | derivation. This option implies --leave-dot-git. 67 | --no-deep-clone Don't include the repository history in the output 68 | derivation. 69 | --verbose, -v Print additional information about the programs 70 | execution. This is useful if you want to issue a bug 71 | report. 72 | --quiet, -q Print less information about the programs execution. 73 | --nix Output the results as valid nix code. 74 | --json Output the results in the JSON format 75 | --meta Output the results in JSON format where the arguments 76 | to fetchFromGitHub are located under the src key of 77 | the resulting json dictionary and meta information 78 | about the prefetched repository is located under the 79 | meta key of the output. 80 | --version show program's version number and exit 81 | --rev REV 82 | #+end_example 83 | 84 | ** nix-prefetch-github-directory 85 | This command examins the current working directory and tries to 86 | figure out if it is part of a git repository linked to github. If 87 | this was successful the program prefetches the currently checked 88 | out commit from the =origin= remote repository similar to the 89 | command =nix-prefetch-github=. 90 | 91 | #+begin_src sh :results verbatim :wrap example :exports results 92 | result/bin/nix-prefetch-github-directory --help 93 | #+end_src 94 | 95 | #+RESULTS: 96 | #+begin_example 97 | usage: .nix-prefetch-github-directory-wrapped [-h] [--fetch-submodules] 98 | [--no-fetch-submodules] 99 | [--leave-dot-git] 100 | [--no-leave-dot-git] 101 | [--deep-clone] [--no-deep-clone] 102 | [--verbose] [--quiet] [--nix] 103 | [--json] [--meta] [--version] 104 | [--directory DIRECTORY] 105 | [--remote REMOTE] 106 | 107 | options: 108 | -h, --help show this help message and exit 109 | --fetch-submodules Include git submodules in the output derivation 110 | --no-fetch-submodules 111 | Don't include git submodules in output derivation 112 | --leave-dot-git Include .git folder in output derivation. Use this if 113 | you need repository data, e.g. current commit hash, 114 | for the build process. 115 | --no-leave-dot-git Don't include .git folder in output derivation. 116 | --deep-clone Include all of the repository history in the output 117 | derivation. This option implies --leave-dot-git. 118 | --no-deep-clone Don't include the repository history in the output 119 | derivation. 120 | --verbose, -v Print additional information about the programs 121 | execution. This is useful if you want to issue a bug 122 | report. 123 | --quiet, -q Print less information about the programs execution. 124 | --nix Output the results as valid nix code. 125 | --json Output the results in the JSON format 126 | --meta Output the results in JSON format where the arguments 127 | to fetchFromGitHub are located under the src key of 128 | the resulting json dictionary and meta information 129 | about the prefetched repository is located under the 130 | meta key of the output. 131 | --version show program's version number and exit 132 | --directory DIRECTORY 133 | --remote REMOTE 134 | #+end_example 135 | 136 | ** nix-prefetch-github-latest-release 137 | This command fetches the code for the latest release of the 138 | specified repository. 139 | 140 | #+begin_src sh :results verbatim :wrap example :exports results 141 | result/bin/nix-prefetch-github-latest-release --help 142 | #+end_src 143 | 144 | #+RESULTS: 145 | #+begin_example 146 | usage: nix-prefetch-github-latest-release [-h] [--fetch-submodules] 147 | [--no-fetch-submodules] 148 | [--leave-dot-git] 149 | [--no-leave-dot-git] [--deep-clone] 150 | [--no-deep-clone] [--verbose] 151 | [--quiet] [--nix] [--json] [--meta] 152 | [--version] 153 | owner repo 154 | 155 | positional arguments: 156 | owner 157 | repo 158 | 159 | options: 160 | -h, --help show this help message and exit 161 | --fetch-submodules Include git submodules in the output derivation 162 | --no-fetch-submodules 163 | Don't include git submodules in output derivation 164 | --leave-dot-git Include .git folder in output derivation. Use this if 165 | you need repository data, e.g. current commit hash, 166 | for the build process. 167 | --no-leave-dot-git Don't include .git folder in output derivation. 168 | --deep-clone Include all of the repository history in the output 169 | derivation. This option implies --leave-dot-git. 170 | --no-deep-clone Don't include the repository history in the output 171 | derivation. 172 | --verbose, -v Print additional information about the programs 173 | execution. This is useful if you want to issue a bug 174 | report. 175 | --quiet, -q Print less information about the programs execution. 176 | --nix Output the results as valid nix code. 177 | --json Output the results in the JSON format 178 | --meta Output the results in JSON format where the arguments 179 | to fetchFromGitHub are located under the src key of 180 | the resulting json dictionary and meta information 181 | about the prefetched repository is located under the 182 | meta key of the output. 183 | --version show program's version number and exit 184 | #+end_example 185 | 186 | * development environment 187 | Use =nix develop= with flake support enabled. Development without 188 | nix flake support is not officially supported. Run the provided 189 | tests via =pytest=. You can control what kind of tests are run via 190 | the variable =DISABLED_TESTS=: 191 | 192 | #+begin_example 193 | # Only run tests that don't hit network and don't use nix 194 | DISABLED_TESTS="network requires_nix_build" pytest 195 | #+end_example 196 | 197 | Currently =network= and =requires_nix_build= are the only values 198 | that make sense with this environment variable. 199 | 200 | You can visualize the dependency graph of the individual python 201 | modules via the =./generate-dependency-graph= program. 202 | 203 | You can generate a coverage report for the tests via 204 | 205 | #+begin_example 206 | coverage run -m nix_prefetch_github.run_tests && coverage html 207 | #+end_example 208 | 209 | 210 | 211 | * changes 212 | ** v8.0.0 (not released yet) 213 | - Drop official support for Python versions <3.11 and introduce 214 | official support for Python version 3.12 215 | - Drop nix-build based prefetcher. This means that users need to 216 | have =nix-prefetch-git= and =nix-prefetch=url= available in their 217 | PATH. 218 | - Meta information the program outputs now contains the store path 219 | of prefetched repositories. 220 | 221 | ** v7.1.0 222 | - Add =-q= / =--quiet= option to decrease logging verbosity 223 | - Add =--meta= option to include the commit timestamp of the latest 224 | prefetched commit in the output 225 | - Use content of ==GITHUB_TOKEN== environment variable for 226 | authenticating with GitHub API 227 | 228 | ** v7.0.0 229 | - The output format changed. In previous versions the json and nix 230 | output included =sha256= as a field. This field was removed in 231 | favour of a =hash= field. The value of this field is an SRI hash. 232 | 233 | ** v6.0.1 234 | - Fix bug in repository detection for 235 | =nix-prefetch-github-directory= 236 | 237 | ** v6.0.0 238 | - Drop support for python3.8 239 | - Drop default arguments to fetchFromGitHub from json output 240 | (e.g. =leaveDotGit = false;=, =fetchSubmodule = false;=, 241 | =deepClone = false;=) 242 | 243 | ** v5.2.2 244 | - Add more info to error messages 245 | 246 | ** v5.2.1 247 | - Fixed a bug that broke the program for users without the 248 | experimental `nix-command` feature 249 | 250 | ** v5.2.0 251 | - Emit warning if unsafe options --deep-clone and --leave-dot-git 252 | are used. 253 | - Improve --help output slightly 254 | - Declutter verbose logging output 255 | 256 | ** v5.1.2 257 | - Use old prefetch implementation because of bug in 258 | =nix-prefetch-git=. See [[https://github.com/NixOS/nixpkgs/issues/168147][this github issue]] 259 | ** v5.1.1 260 | - Fix bug that broke =nix-prefetch-github --version= 261 | 262 | ** v5.1.0 263 | - Use =nix-prefetch-git= and =nix-prefetch-url= for calculating 264 | sha256 sums when possible. The application will fall back to the 265 | old method when =nix-prefetch-*= are not available. 266 | 267 | ** v5.0.1 268 | - Fix breaking bug in hash generation 269 | 270 | ** v5.0.0 271 | - Remove all dependencies to other python packages other than 272 | "core" ones 273 | - Allow users to control debugging output via the =--verbosity= cli 274 | option 275 | - All commands now understand =--fetch-submodules= and 276 | =--no-fetch-submodules= options 277 | - Commands now understand =--leave-dot-git= and 278 | =--no-leave-dot-git= options 279 | - Commands now understand =--deep-clone= and =--no-deep-clone= 280 | 281 | ** v4.0.4 282 | - Print standard error output of subprocesses for better debugging 283 | 284 | ** v4.0.3 285 | - Generated hashes now don't have a "sha256-" prefix 286 | - jinja2 is no longer a dependency of nix-prefetch-github 287 | 288 | ** v4.0.2 289 | - packaging release, no bugfixes or features 290 | 291 | ** v4.0.1 292 | - Fix issue #38 293 | 294 | ** v4.0 295 | - Make fetching submodules the default in calls to python 296 | routines. The CLI should be uneffected by this change. 297 | - Remove default values for =fetch_submodules= in all internal 298 | classes. 299 | - Implement =nix-prefetch-github-latest-release= command 300 | 301 | ** v3.0 302 | - major changes to the internal module structure 303 | - introduction of the =nix-prefetch-github-directory= command 304 | - code repository now functions as a nix flake 305 | 306 | ** v2.4 307 | - added =--fetch-submodules= flag 308 | - Fixed incompability with nix 2.4 309 | 310 | ** v2.3.2 311 | - fix issues #21, #22 312 | - nix-prefetch-github now accepts full ref names, e.g. 313 | =refs/heads/master= which was broken since 2.3 (#23) 314 | 315 | ** v2.3.1 316 | - Fix bug in generated nix expression 317 | - Fix bug that prevented targeting tags with prefetch command 318 | - Improve error message format in case revision is not found 319 | 320 | ** v2.3 321 | - Remove dependency to =requests= 322 | - Default to =master= branch instead of first branch in list 323 | 324 | ** v2.2 325 | - Add =--version= flag 326 | - Fix bug in output formatting 327 | 328 | ** v2.1 329 | - Fix bug (#4) that made =nix-prefetch-github= incompatible with 330 | =nix 2.2=. 331 | 332 | ** v2.0 333 | - The result of nix_pretch_github and its corresponding command 334 | line tool now contains always the actual commit hash as detected 335 | by the tool instead of the branch or tag name. 336 | - Add a new flag =--nix= that makes the command line tool output a 337 | valid nix expression 338 | - Removed the =--hash-only= and =--no-hash-only= flags and changed 339 | add =--prefetch= and =--no-prefetch= flags to replace them. 340 | -------------------------------------------------------------------------------- /docs/README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | This module implements a python function and a command line tool to help 5 | you fetch sources from github when using ``fetchFromGitHub``. 6 | 7 | This program can be distributed under the conditions of the GNU Public 8 | License Version 3. Check out ``LICENSE.txt`` to read the license text. 9 | 10 | Dependencies 11 | ============ 12 | 13 | - Python and its standard library 14 | - nix-prefetch-url 15 | - nix-prefech-github 16 | - git 17 | - nix 18 | 19 | Command Line Example 20 | ==================== 21 | 22 | .. code:: bash 23 | 24 | result/bin/nix-prefetch-github seppeljordan nix-prefetch-github 25 | 26 | :: 27 | 28 | { 29 | "owner": "seppeljordan", 30 | "repo": "nix-prefetch-github", 31 | "rev": "9578399cadb1cb2b252438cf14663333e8c3ee00", 32 | "hash": "sha256-JFC1+y+FMs2TwWjJxlAKAyDbSLFBE9J65myp7+slp50=" 33 | } 34 | 35 | Available Commands 36 | ================== 37 | 38 | .. _nix-prefetch-github-1: 39 | 40 | nix-prefetch-github 41 | ------------------- 42 | 43 | This command downloads the code from a github repository and puts it 44 | into the local nix store. It also prints the function arguments to 45 | ``fetchFromGitHub`` to the standard output. : 46 | 47 | :: 48 | 49 | usage: nix-prefetch-github [-h] [--fetch-submodules] [--no-fetch-submodules] 50 | [--leave-dot-git] [--no-leave-dot-git] 51 | [--deep-clone] [--no-deep-clone] [--verbose] 52 | [--quiet] [--nix] [--json] [--meta] [--version] 53 | [--rev REV] 54 | owner repo 55 | 56 | positional arguments: 57 | owner 58 | repo 59 | 60 | options: 61 | -h, --help show this help message and exit 62 | --fetch-submodules Include git submodules in the output derivation 63 | --no-fetch-submodules 64 | Don't include git submodules in output derivation 65 | --leave-dot-git Include .git folder in output derivation. Use this if 66 | you need repository data, e.g. current commit hash, 67 | for the build process. 68 | --no-leave-dot-git Don't include .git folder in output derivation. 69 | --deep-clone Include all of the repository history in the output 70 | derivation. This option implies --leave-dot-git. 71 | --no-deep-clone Don't include the repository history in the output 72 | derivation. 73 | --verbose, -v Print additional information about the programs 74 | execution. This is useful if you want to issue a bug 75 | report. 76 | --quiet, -q Print less information about the programs execution. 77 | --nix Output the results as valid nix code. 78 | --json Output the results in the JSON format 79 | --meta Output the results in JSON format where the arguments 80 | to fetchFromGitHub are located under the src key of 81 | the resulting json dictionary and meta information 82 | about the prefetched repository is located under the 83 | meta key of the output. 84 | --version show program's version number and exit 85 | --rev REV 86 | 87 | nix-prefetch-github-directory 88 | ----------------------------- 89 | 90 | This command examins the current working directory and tries to figure 91 | out if it is part of a git repository linked to github. If this was 92 | successful the program prefetches the currently checked out commit from 93 | the ``origin`` remote repository similar to the command 94 | ``nix-prefetch-github``. 95 | 96 | :: 97 | 98 | usage: .nix-prefetch-github-directory-wrapped [-h] [--fetch-submodules] 99 | [--no-fetch-submodules] 100 | [--leave-dot-git] 101 | [--no-leave-dot-git] 102 | [--deep-clone] [--no-deep-clone] 103 | [--verbose] [--quiet] [--nix] 104 | [--json] [--meta] [--version] 105 | [--directory DIRECTORY] 106 | [--remote REMOTE] 107 | 108 | options: 109 | -h, --help show this help message and exit 110 | --fetch-submodules Include git submodules in the output derivation 111 | --no-fetch-submodules 112 | Don't include git submodules in output derivation 113 | --leave-dot-git Include .git folder in output derivation. Use this if 114 | you need repository data, e.g. current commit hash, 115 | for the build process. 116 | --no-leave-dot-git Don't include .git folder in output derivation. 117 | --deep-clone Include all of the repository history in the output 118 | derivation. This option implies --leave-dot-git. 119 | --no-deep-clone Don't include the repository history in the output 120 | derivation. 121 | --verbose, -v Print additional information about the programs 122 | execution. This is useful if you want to issue a bug 123 | report. 124 | --quiet, -q Print less information about the programs execution. 125 | --nix Output the results as valid nix code. 126 | --json Output the results in the JSON format 127 | --meta Output the results in JSON format where the arguments 128 | to fetchFromGitHub are located under the src key of 129 | the resulting json dictionary and meta information 130 | about the prefetched repository is located under the 131 | meta key of the output. 132 | --version show program's version number and exit 133 | --directory DIRECTORY 134 | --remote REMOTE 135 | 136 | nix-prefetch-github-latest-release 137 | ---------------------------------- 138 | 139 | This command fetches the code for the latest release of the specified 140 | repository. 141 | 142 | :: 143 | 144 | usage: nix-prefetch-github-latest-release [-h] [--fetch-submodules] 145 | [--no-fetch-submodules] 146 | [--leave-dot-git] 147 | [--no-leave-dot-git] [--deep-clone] 148 | [--no-deep-clone] [--verbose] 149 | [--quiet] [--nix] [--json] [--meta] 150 | [--version] 151 | owner repo 152 | 153 | positional arguments: 154 | owner 155 | repo 156 | 157 | options: 158 | -h, --help show this help message and exit 159 | --fetch-submodules Include git submodules in the output derivation 160 | --no-fetch-submodules 161 | Don't include git submodules in output derivation 162 | --leave-dot-git Include .git folder in output derivation. Use this if 163 | you need repository data, e.g. current commit hash, 164 | for the build process. 165 | --no-leave-dot-git Don't include .git folder in output derivation. 166 | --deep-clone Include all of the repository history in the output 167 | derivation. This option implies --leave-dot-git. 168 | --no-deep-clone Don't include the repository history in the output 169 | derivation. 170 | --verbose, -v Print additional information about the programs 171 | execution. This is useful if you want to issue a bug 172 | report. 173 | --quiet, -q Print less information about the programs execution. 174 | --nix Output the results as valid nix code. 175 | --json Output the results in the JSON format 176 | --meta Output the results in JSON format where the arguments 177 | to fetchFromGitHub are located under the src key of 178 | the resulting json dictionary and meta information 179 | about the prefetched repository is located under the 180 | meta key of the output. 181 | --version show program's version number and exit 182 | 183 | development environment 184 | ======================= 185 | 186 | Use ``nix develop`` with flake support enabled. Development without nix 187 | flake support is not officially supported. Run the provided tests via 188 | ``pytest``. You can control what kind of tests are run via the variable 189 | ``DISABLED_TESTS``: 190 | 191 | :: 192 | 193 | # Only run tests that don't hit network and don't use nix 194 | DISABLED_TESTS="network requires_nix_build" pytest 195 | 196 | Currently ``network`` and ``requires_nix_build`` are the only values 197 | that make sense with this environment variable. 198 | 199 | You can visualize the dependency graph of the individual python modules 200 | via the ``./generate-dependency-graph`` program. 201 | 202 | You can generate a coverage report for the tests via 203 | 204 | :: 205 | 206 | coverage run -m nix_prefetch_github.run_tests && coverage html 207 | 208 | changes 209 | ======= 210 | 211 | v8.0.0 (not released yet) 212 | ------------------------- 213 | 214 | - Drop official support for Python versions <3.11 and introduce 215 | official support for Python version 3.12 216 | - Drop nix-build based prefetcher. This means that users need to have 217 | ``nix-prefetch-git`` and ``nix-prefetch=url`` available in their 218 | PATH. 219 | - Meta information the program outputs now contains the store path of 220 | prefetched repositories. 221 | 222 | v7.1.0 223 | ------ 224 | 225 | - Add ``-q`` / ``--quiet`` option to decrease logging verbosity 226 | - Add ``--meta`` option to include the commit timestamp of the latest 227 | prefetched commit in the output 228 | - Use content of ``=GITHUB_TOKEN=`` environment variable for 229 | authenticating with GitHub API 230 | 231 | v7.0.0 232 | ------ 233 | 234 | - The output format changed. In previous versions the json and nix 235 | output included ``sha256`` as a field. This field was removed in 236 | favour of a ``hash`` field. The value of this field is an SRI hash. 237 | 238 | v6.0.1 239 | ------ 240 | 241 | - Fix bug in repository detection for ``nix-prefetch-github-directory`` 242 | 243 | v6.0.0 244 | ------ 245 | 246 | - Drop support for python3.8 247 | - Drop default arguments to fetchFromGitHub from json output (e.g. 248 | ``leaveDotGit = false;``, ``fetchSubmodule = false;``, 249 | ``deepClone = false;``) 250 | 251 | v5.2.2 252 | ------ 253 | 254 | - Add more info to error messages 255 | 256 | v5.2.1 257 | ------ 258 | 259 | - Fixed a bug that broke the program for users without the experimental 260 | \`nix-command\` feature 261 | 262 | v5.2.0 263 | ------ 264 | 265 | - Emit warning if unsafe options –deep-clone and –leave-dot-git are 266 | used. 267 | - Improve –help output slightly 268 | - Declutter verbose logging output 269 | 270 | v5.1.2 271 | ------ 272 | 273 | - Use old prefetch implementation because of bug in 274 | ``nix-prefetch-git``. See `this github 275 | issue `__ 276 | 277 | v5.1.1 278 | ------ 279 | 280 | - Fix bug that broke ``nix-prefetch-github --version`` 281 | 282 | v5.1.0 283 | ------ 284 | 285 | - Use ``nix-prefetch-git`` and ``nix-prefetch-url`` for calculating 286 | sha256 sums when possible. The application will fall back to the old 287 | method when ``nix-prefetch-*`` are not available. 288 | 289 | v5.0.1 290 | ------ 291 | 292 | - Fix breaking bug in hash generation 293 | 294 | v5.0.0 295 | ------ 296 | 297 | - Remove all dependencies to other python packages other than "core" 298 | ones 299 | - Allow users to control debugging output via the ``--verbosity`` cli 300 | option 301 | - All commands now understand ``--fetch-submodules`` and 302 | ``--no-fetch-submodules`` options 303 | - Commands now understand ``--leave-dot-git`` and 304 | ``--no-leave-dot-git`` options 305 | - Commands now understand ``--deep-clone`` and ``--no-deep-clone`` 306 | 307 | v4.0.4 308 | ------ 309 | 310 | - Print standard error output of subprocesses for better debugging 311 | 312 | v4.0.3 313 | ------ 314 | 315 | - Generated hashes now don't have a "sha256-" prefix 316 | - jinja2 is no longer a dependency of nix-prefetch-github 317 | 318 | v4.0.2 319 | ------ 320 | 321 | - packaging release, no bugfixes or features 322 | 323 | v4.0.1 324 | ------ 325 | 326 | - Fix issue #38 327 | 328 | v4.0 329 | ---- 330 | 331 | - Make fetching submodules the default in calls to python routines. The 332 | CLI should be uneffected by this change. 333 | - Remove default values for ``fetch_submodules`` in all internal 334 | classes. 335 | - Implement ``nix-prefetch-github-latest-release`` command 336 | 337 | v3.0 338 | ---- 339 | 340 | - major changes to the internal module structure 341 | - introduction of the ``nix-prefetch-github-directory`` command 342 | - code repository now functions as a nix flake 343 | 344 | v2.4 345 | ---- 346 | 347 | - added ``--fetch-submodules`` flag 348 | - Fixed incompability with nix 2.4 349 | 350 | v2.3.2 351 | ------ 352 | 353 | - fix issues #21, #22 354 | - nix-prefetch-github now accepts full ref names, e.g. 355 | ``refs/heads/master`` which was broken since 2.3 (#23) 356 | 357 | v2.3.1 358 | ------ 359 | 360 | - Fix bug in generated nix expression 361 | - Fix bug that prevented targeting tags with prefetch command 362 | - Improve error message format in case revision is not found 363 | 364 | v2.3 365 | ---- 366 | 367 | - Remove dependency to ``requests`` 368 | - Default to ``master`` branch instead of first branch in list 369 | 370 | v2.2 371 | ---- 372 | 373 | - Add ``--version`` flag 374 | - Fix bug in output formatting 375 | 376 | v2.1 377 | ---- 378 | 379 | - Fix bug (#4) that made ``nix-prefetch-github`` incompatible with 380 | ``nix 2.2``. 381 | 382 | v2.0 383 | ---- 384 | 385 | - The result of nix\ :sub:`pretchgithub` and its corresponding command 386 | line tool now contains always the actual commit hash as detected by 387 | the tool instead of the branch or tag name. 388 | - Add a new flag ``--nix`` that makes the command line tool output a 389 | valid nix expression 390 | - Removed the ``--hash-only`` and ``--no-hash-only`` flags and changed 391 | add ``--prefetch`` and ``--no-prefetch`` flags to replace them. 392 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import List 3 | 4 | from nix_prefetch_github.version import VERSION_STRING 5 | 6 | sys.path.append("..") 7 | project = "nix-prefetch-github" 8 | copyright = "2022, Sebastian Jordan" 9 | author = "Sebastian Jordan" 10 | release = VERSION_STRING 11 | extensions = ["sphinxarg.ext"] 12 | templates_path = ["_templates"] 13 | exclude_patterns: List[str] = [] 14 | html_theme = "alabaster" 15 | html_static_path = ["_static"] 16 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to nix-prefetch-github's documentation! 2 | =============================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | 9 | Available programs 10 | ================== 11 | 12 | nix-prefetch-github 13 | ------------------- 14 | 15 | .. argparse:: 16 | :module: nix_prefetch_github.controller.nix_prefetch_github_controller 17 | :func: get_argument_parser 18 | :prog: nix-prefetch-github 19 | 20 | Use this program to generate the arguments for ``fetchFromGitHub`` 21 | nix function. 22 | 23 | nix-prefetch-github-directory 24 | ----------------------------- 25 | 26 | .. argparse:: 27 | :module: nix_prefetch_github.controller.nix_prefetch_github_directory_controller 28 | :func: get_argument_parser 29 | :prog: nix-prefetch-github-directory 30 | 31 | Use this program to generate a nix expression for 32 | ``fetchFromGitHub`` based on the git repository in the local 33 | directory. 34 | 35 | nix-prefetch-latest-release 36 | --------------------------- 37 | 38 | .. argparse:: 39 | :module: nix_prefetch_github.controller.nix_prefetch_github_latest_release_controller 40 | :func: get_argument_parser 41 | :prog: nix-prefetch-github-latest-release 42 | 43 | Use this program to generate a nix expression for the latest 44 | release of a github repository. 45 | 46 | Configuration 47 | ============= 48 | 49 | Github API Token 50 | ---------------- 51 | 52 | All the commands provided by this package will read the environment 53 | variable ``GITHUB_TOKEN`` and use its content verbatim as an 54 | authentication/authorization token when requesting from GitHubs API. 55 | 56 | output formats 57 | ============== 58 | 59 | The different command line programs of this package provide different 60 | output formats. Those can be selected via different arguments provided 61 | to the program. 62 | 63 | JSON output 64 | ----------- 65 | 66 | The ``--json`` argument is the default and will render 67 | the output as JSON to the standard output of the program. The 68 | resulting json value will consist of a single dictionary where all key 69 | value pairs correspond to the arguments to the ``fetchFromGitHub`` 70 | function provided by ``nixpkgs``. It is intended to be used directly in 71 | nix code:: 72 | 73 | src = with builtins; fetchFromGitHub (fromJSON (readFile ./nix-prefetch-github-output.json)) 74 | 75 | 76 | Meta information output 77 | ----------------------- 78 | 79 | The ``--meta`` argument produces output similar to the output produced 80 | by ``--json``. The difference is that a nested dictionary is produced 81 | with two top level keys: ``src`` and ``meta``. The ``src`` key will 82 | contain the same data as would have been yielded by ``--json`` whereas 83 | the ``meta`` key contains some meta data on the commit that was 84 | fetched. At the moment this will only be the date and time of day of 85 | the latest commit. This can be useful when providing nightly versions 86 | of a package:: 87 | 88 | let fetchedSourceCode = (fromJSON (readFile ./nix-prefetch-github-output.json)); 89 | in buildPackage { 90 | pname = "my-package"; 91 | version = fetchedSourceCode.meta.commitDate; 92 | src = fetchFromGitHub fetchedSourceCode.src; 93 | ... 94 | }; 95 | 96 | Nix output 97 | ---------- 98 | 99 | Consider this a legacy output format without any practical 100 | applications. The ``--nix`` argument will result in a valid nix 101 | expression produced by the respective program. 102 | -------------------------------------------------------------------------------- /elisp/update-readme.el: -------------------------------------------------------------------------------- 1 | (progn 2 | (setq org-confirm-babel-evaluate 3 | (lambda (lang body) (not (string= lang "sh")))) 4 | (org-babel-do-load-languages 5 | 'org-babel-load-languages 6 | '((shell . t))) 7 | (org-babel-execute-buffer) 8 | (save-buffer)) 9 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1737885589, 24 | "narHash": "sha256-Zf0hSrtzaM1DEz8//+Xs51k/wdSajticVrATqDrfQjg=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "852ff1d9e153d8875a83602e03fdef8a63f0ecf8", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs-stable": { 38 | "locked": { 39 | "lastModified": 1738023785, 40 | "narHash": "sha256-BPHmb3fUwdHkonHyHi1+x89eXB3kA1jffIpwPVJIVys=", 41 | "owner": "NixOS", 42 | "repo": "nixpkgs", 43 | "rev": "2b4230bf03deb33103947e2528cac2ed516c5c89", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "NixOS", 48 | "ref": "nixos-24.11", 49 | "repo": "nixpkgs", 50 | "type": "github" 51 | } 52 | }, 53 | "root": { 54 | "inputs": { 55 | "flake-utils": "flake-utils", 56 | "nixpkgs": "nixpkgs", 57 | "nixpkgs-stable": "nixpkgs-stable" 58 | } 59 | }, 60 | "systems": { 61 | "locked": { 62 | "lastModified": 1681028828, 63 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 64 | "owner": "nix-systems", 65 | "repo": "default", 66 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "nix-systems", 71 | "repo": "default", 72 | "type": "github" 73 | } 74 | } 75 | }, 76 | "root": "root", 77 | "version": 7 78 | } 79 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "nix-prefetch-github"; 3 | 4 | inputs = { 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 7 | nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-24.11"; 8 | }; 9 | 10 | outputs = { self, nixpkgs, flake-utils, nixpkgs-stable, ... }: 11 | let 12 | systemDependent = flake-utils.lib.eachSystem supportedSystems (system: 13 | let 14 | pkgs = import nixpkgs { 15 | inherit system; 16 | overlays = [ self.overlays.default ]; 17 | }; 18 | pkgsStable = import nixpkgs-stable { 19 | inherit system; 20 | overlays = [ self.overlays.default ]; 21 | }; 22 | python = pkgs.python3; 23 | in { 24 | packages = { default = pkgs.nix-prefetch-github; }; 25 | devShells = { 26 | default = pkgs.callPackage nix/dev-shell.nix { }; 27 | stable = pkgsStable.callPackage nix/dev-shell.nix { }; 28 | }; 29 | checks = { 30 | defaultPackage = self.packages.${system}.default; 31 | nixosStablePackage = pkgsStable.nix-prefetch-github; 32 | nix-prefetch-github-python311 = 33 | pkgs.python311.pkgs.nix-prefetch-github; 34 | nix-prefetch-github-python312 = 35 | pkgs.python312.pkgs.nix-prefetch-github; 36 | black-check = pkgs.runCommand "black-nix-prefetch-github" { } '' 37 | cd ${self} 38 | ${python.pkgs.black}/bin/black --check . 39 | mkdir $out 40 | ''; 41 | mypy-check = pkgs.runCommand "mypy-nix-prefetch-github" { } '' 42 | cd ${self} 43 | ${python.pkgs.mypy}/bin/mypy nix_prefetch_github tests 44 | mkdir $out 45 | ''; 46 | isort-check = pkgs.runCommand "isort-nix-prefetch-github" { } '' 47 | cd ${self} 48 | ${python.pkgs.isort}/bin/isort \ 49 | --settings-path setup.cfg \ 50 | --check-only \ 51 | . \ 52 | test-pypi-install 53 | mkdir $out 54 | ''; 55 | flake8-check = pkgs.runCommand "flake8-nix-prefetch-github" { } '' 56 | cd ${self} 57 | ${python.pkgs.flake8}/bin/flake8 58 | mkdir $out 59 | ''; 60 | nixfmt-check = pkgs.runCommand "nixfmt-nix-prefetch-github" { } '' 61 | ${pkgs.nixfmt}/bin/nixfmt --check \ 62 | $(find ${self} -type f -name '*.nix') 63 | mkdir $out 64 | ''; 65 | }; 66 | }); 67 | systemIndependent = { 68 | overlays.default = final: prev: { 69 | pythonPackagesExtensions = prev.pythonPackagesExtensions 70 | ++ [ (import nix/package-overrides.nix) ]; 71 | }; 72 | }; 73 | supportedSystems = flake-utils.lib.defaultSystems; 74 | in systemDependent // systemIndependent; 75 | } 76 | -------------------------------------------------------------------------------- /format-code: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import subprocess 5 | from os import path 6 | 7 | 8 | def main(): 9 | format_nix_files() 10 | subprocess.run("black . test-pypi-install format-code", shell=True) 11 | subprocess.run("isort . test-pypi-install format-code", shell=True) 12 | 13 | 14 | def format_nix_files(): 15 | nix_files = ( 16 | path.join(directory, filename) 17 | for directory, _, filenames in os.walk(".") 18 | for filename in filenames 19 | if filename.endswith(".nix") 20 | ) 21 | for nix_file in nix_files: 22 | check_process = subprocess.run(["nixfmt", "--check", nix_file]) 23 | if check_process.returncode != 0: 24 | subprocess.run(["nixfmt", nix_file]) 25 | 26 | 27 | if __name__ == "__main__": 28 | main() 29 | -------------------------------------------------------------------------------- /generate-dependency-graph: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | pydeps nix_prefetch_github -x '*.test_*' -xx 'nix_prefetch_github.tests' -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /nix/dev-shell.nix: -------------------------------------------------------------------------------- 1 | { mkShell, git, nixfmt, nix-prefetch-scripts, pandoc, python3 }: 2 | mkShell { 3 | packages = [ git nixfmt nix-prefetch-scripts pandoc ] ++ (with python3.pkgs; [ 4 | black 5 | flake8 6 | mypy 7 | twine 8 | virtualenv 9 | isort 10 | coverage 11 | pydeps 12 | pip 13 | ]); 14 | inputsFrom = [ python3.pkgs.nix-prefetch-github ]; 15 | } 16 | -------------------------------------------------------------------------------- /nix/nix-prefetch-github.nix: -------------------------------------------------------------------------------- 1 | { buildPythonPackage, git, which, sphinxHook, sphinx-argparse, pytestCheckHook 2 | 3 | , parameterized }: 4 | let version = builtins.readFile ../nix_prefetch_github/VERSION; 5 | in buildPythonPackage { 6 | pname = "nix-prefetch-github"; 7 | version = "${version}-dev"; 8 | outputs = [ "out" "doc" "man" ]; 9 | src = ../.; 10 | nativeBuildInputs = [ sphinxHook sphinx-argparse ]; 11 | nativeCheckInputs = [ git which pytestCheckHook ]; 12 | postInstall = '' 13 | for f in $out/bin/* ; do 14 | wrapProgram "$f" --suffix PATH : "${git}/bin" 15 | done 16 | ''; 17 | checkInputs = [ git which pytestCheckHook parameterized ]; 18 | sphinxBuilders = [ "singlehtml" "man" ]; 19 | sphinxRoot = "docs"; 20 | DISABLED_TESTS = "network requires_nix_build"; 21 | } 22 | -------------------------------------------------------------------------------- /nix/package-overrides.nix: -------------------------------------------------------------------------------- 1 | self: super: { 2 | nix-prefetch-github = self.callPackage ./nix-prefetch-github.nix { }; 3 | } 4 | -------------------------------------------------------------------------------- /nix_prefetch_github/VERSION: -------------------------------------------------------------------------------- 1 | 7.1.0 -------------------------------------------------------------------------------- /nix_prefetch_github/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seppeljordan/nix-prefetch-github/8a4d3cac69a0fc8aaa6ec9f310067bdc82ace788/nix_prefetch_github/__init__.py -------------------------------------------------------------------------------- /nix_prefetch_github/__main__.py: -------------------------------------------------------------------------------- 1 | from nix_prefetch_github.cli.fetch_github import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /nix_prefetch_github/alerter.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from logging import Logger 3 | 4 | from nix_prefetch_github.interfaces import PrefetchOptions 5 | 6 | 7 | @dataclass 8 | class CliAlerterImpl: 9 | logger: Logger 10 | 11 | def alert_user_about_unsafe_prefetch_options( 12 | self, prefetch_options: PrefetchOptions 13 | ) -> None: 14 | if prefetch_options.deep_clone: 15 | self._emit_option_warning("--deep-clone") 16 | if prefetch_options.leave_dot_git: 17 | self._emit_option_warning("--leave-dot-git") 18 | 19 | def _emit_option_warning(self, option: str) -> None: 20 | issue = "https://github.com/NixOS/nixpkgs/issues/8567" 21 | self.logger.warning( 22 | "%s was used. The resulting fetchFromGitHub directive might be non deterministic. Check %s for more information", 23 | option, 24 | issue, 25 | ) 26 | -------------------------------------------------------------------------------- /nix_prefetch_github/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seppeljordan/nix-prefetch-github/8a4d3cac69a0fc8aaa6ec9f310067bdc82ace788/nix_prefetch_github/cli/__init__.py -------------------------------------------------------------------------------- /nix_prefetch_github/cli/fetch_directory.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from nix_prefetch_github.dependency_injector import DependencyInjector 4 | 5 | 6 | def main() -> None: 7 | injector = DependencyInjector() 8 | controller = injector.get_prefetch_directory_controller() 9 | controller.process_arguments(sys.argv[1:]) 10 | 11 | 12 | if __name__ == "__main__": 13 | main() 14 | -------------------------------------------------------------------------------- /nix_prefetch_github/cli/fetch_github.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from nix_prefetch_github.dependency_injector import DependencyInjector 4 | 5 | 6 | def main() -> None: 7 | injector = DependencyInjector() 8 | controller = injector.get_prefetch_github_repository_controller() 9 | controller.process_arguments(sys.argv[1:]) 10 | 11 | 12 | if __name__ == "__main__": 13 | main() 14 | -------------------------------------------------------------------------------- /nix_prefetch_github/cli/fetch_latest_release.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from nix_prefetch_github.dependency_injector import DependencyInjector 4 | 5 | 6 | def main() -> None: 7 | injector = DependencyInjector() 8 | controller = injector.get_prefetch_latest_release_controller() 9 | controller.process_arguments(sys.argv[1:]) 10 | 11 | 12 | if __name__ == "__main__": 13 | main() 14 | -------------------------------------------------------------------------------- /nix_prefetch_github/command/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seppeljordan/nix-prefetch-github/8a4d3cac69a0fc8aaa6ec9f310067bdc82ace788/nix_prefetch_github/command/__init__.py -------------------------------------------------------------------------------- /nix_prefetch_github/command/command_runner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shlex 3 | import subprocess 4 | from dataclasses import dataclass 5 | from logging import Logger 6 | from typing import Dict, List, Optional, Tuple 7 | 8 | 9 | @dataclass(frozen=True) 10 | class CommandRunnerImpl: 11 | logger: Logger 12 | 13 | def run_command( 14 | self, 15 | command: List[str], 16 | cwd: Optional[str] = None, 17 | environment_variables: Optional[Dict[str, str]] = None, 18 | merge_stderr: bool = False, 19 | ) -> Tuple[int, str]: 20 | if environment_variables is None: 21 | environment_variables = dict() 22 | target_environment = dict(os.environ, **environment_variables) 23 | stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE 24 | self.logger.info("Running command: %s", shlex.join(command)) 25 | process = subprocess.Popen( 26 | command, 27 | stdout=subprocess.PIPE, 28 | stderr=stderr, 29 | universal_newlines=True, 30 | cwd=cwd, 31 | env=target_environment, 32 | ) 33 | process_stdout, process_stderr = process.communicate() 34 | if merge_stderr: 35 | self._log_process_output(process_stdout) 36 | else: 37 | self._log_process_output(process_stderr) 38 | return process.returncode, process_stdout 39 | 40 | def _log_process_output(self, process_output: str) -> None: 41 | if process_output: 42 | self.logger.info(process_output) 43 | -------------------------------------------------------------------------------- /nix_prefetch_github/command/test_command_runner.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from io import StringIO 3 | from unittest import TestCase 4 | 5 | from nix_prefetch_github.command.command_runner import CommandRunnerImpl 6 | 7 | 8 | class CommandRunnerTests(TestCase): 9 | def setUp(self) -> None: 10 | self.stream = StringIO() 11 | self.handler = logging.StreamHandler(self.stream) 12 | self.log = logging.getLogger("mylogger") 13 | self.log.setLevel(logging.DEBUG) 14 | for handler in self.log.handlers: 15 | self.log.removeHandler(handler) 16 | self.log.addHandler(self.handler) 17 | self.command_runner = CommandRunnerImpl( 18 | logger=self.log, 19 | ) 20 | 21 | def test_that_for_command_without_stderr_output_only_command_call_is_logged( 22 | self, 23 | ) -> None: 24 | self.command_runner.run_command( 25 | command=["python", "-c", "print('test' 'string')"] 26 | ) 27 | self.assertNotInLogs("teststring") 28 | 29 | def test_that_for_command_with_stderr_output_also_the_command_output_is_logged( 30 | self, 31 | ) -> None: 32 | self.command_runner.run_command( 33 | command=[ 34 | "python", 35 | "-c", 36 | "import sys; print('test' 'string', file=sys.stderr)", 37 | ] 38 | ) 39 | self.assertInLogs("teststring") 40 | 41 | def test_that_stdout_is_logged_if_it_is_merged_with_stderr( 42 | self, 43 | ) -> None: 44 | self.command_runner.run_command( 45 | command=[ 46 | "python", 47 | "-c", 48 | "print('test' 'string')", 49 | ], 50 | merge_stderr=True, 51 | ) 52 | self.assertInLogs("teststring") 53 | 54 | def test_that_empty_string_is_not_logged_if_no_stderr_is_produced(self) -> None: 55 | self.command_runner.run_command( 56 | command=[ 57 | "python", 58 | "-c", 59 | "print('test' 'string')", 60 | ], 61 | ) 62 | self.assertNotInLogs("\n\n") 63 | 64 | def test_that_empty_string_is_not_logged_if_no_stderr_is_produced_and_stdout_is_merged( 65 | self, 66 | ) -> None: 67 | self.command_runner.run_command( 68 | command=[ 69 | "python", 70 | "-c", 71 | "pass", 72 | ], 73 | merge_stderr=True, 74 | ) 75 | self.assertNotInLogs("\n\n") 76 | 77 | def assertInLogs(self, log_output: str) -> None: 78 | self.stream.seek(0) 79 | output = self.stream.read() 80 | self.assertIn(log_output, output) 81 | 82 | def assertNotInLogs(self, log_output: str) -> None: 83 | self.stream.seek(0) 84 | output = self.stream.read() 85 | self.assertNotIn(log_output, output) 86 | -------------------------------------------------------------------------------- /nix_prefetch_github/controller/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seppeljordan/nix-prefetch-github/8a4d3cac69a0fc8aaa6ec9f310067bdc82ace788/nix_prefetch_github/controller/__init__.py -------------------------------------------------------------------------------- /nix_prefetch_github/controller/arguments.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | from logging import WARNING 4 | from typing import Any, Optional, Type 5 | 6 | from nix_prefetch_github.interfaces import PrefetchOptions, RenderingFormat 7 | from nix_prefetch_github.logging import LoggingConfiguration 8 | from nix_prefetch_github.version import VERSION_STRING 9 | 10 | 11 | def set_argument(name: str, value: Any) -> Type[argparse.Action]: 12 | class _SetArgument(argparse.Action): 13 | def __init__( 14 | self, option_strings: Any, dest: Any, nargs: Any = None, **kwargs: Any 15 | ) -> None: 16 | assert nargs is None, "Setting nargs is not allowed" 17 | super().__init__(option_strings, dest, nargs=0, **kwargs) 18 | 19 | def __call__( 20 | self, 21 | parser: argparse.ArgumentParser, 22 | namespace: argparse.Namespace, 23 | values: Any, 24 | option_string: Optional[str] = None, 25 | ) -> None: 26 | setattr(getattr(namespace, self.dest), name, value) 27 | 28 | return _SetArgument 29 | 30 | 31 | class increase_log_level(argparse.Action): 32 | def __init__( 33 | self, option_strings: Any, dest: Any, nargs: Any = None, **kwargs: Any 34 | ) -> None: 35 | assert nargs is None, "Setting nargs is not allowed" 36 | super().__init__(option_strings, dest, nargs=0, **kwargs) 37 | 38 | def __call__( 39 | self, 40 | parser: argparse.ArgumentParser, 41 | namespace: argparse.Namespace, 42 | values: Any, 43 | option_string: Optional[str] = None, 44 | ) -> None: 45 | configuration = getattr(namespace, self.dest) 46 | configuration.increase_log_level() 47 | 48 | 49 | class decrease_log_level(argparse.Action): 50 | def __init__( 51 | self, option_strings: Any, dest: Any, nargs: Any = None, **kwargs: Any 52 | ) -> None: 53 | assert nargs is None, "Setting nargs is not allowed" 54 | super().__init__(option_strings, dest, nargs=0, **kwargs) 55 | 56 | def __call__( 57 | self, 58 | parser: argparse.ArgumentParser, 59 | namespace: argparse.Namespace, 60 | values: Any, 61 | option_string: Optional[str] = None, 62 | ) -> None: 63 | configuration = getattr(namespace, self.dest) 64 | configuration.decrease_log_level() 65 | 66 | 67 | def get_options_argument_parser() -> argparse.ArgumentParser: 68 | parser = argparse.ArgumentParser(add_help=False) 69 | parser.add_argument( 70 | "--fetch-submodules", 71 | dest="prefetch_options", 72 | default=PrefetchOptions(), 73 | action=set_argument("fetch_submodules", True), 74 | help="Include git submodules in the output derivation", 75 | ) 76 | parser.add_argument( 77 | "--no-fetch-submodules", 78 | dest="prefetch_options", 79 | action=set_argument("fetch_submodules", False), 80 | help="Don't include git submodules in output derivation", 81 | ) 82 | parser.add_argument( 83 | "--leave-dot-git", 84 | dest="prefetch_options", 85 | action=set_argument("leave_dot_git", True), 86 | help="Include .git folder in output derivation. Use this if you need repository data, e.g. current commit hash, for the build process.", 87 | ) 88 | parser.add_argument( 89 | "--no-leave-dot-git", 90 | dest="prefetch_options", 91 | action=set_argument("leave_dot_git", False), 92 | help="Don't include .git folder in output derivation.", 93 | ) 94 | parser.add_argument( 95 | "--deep-clone", 96 | dest="prefetch_options", 97 | action=set_argument("deep_clone", True), 98 | help="Include all of the repository history in the output derivation. This option implies --leave-dot-git.", 99 | ) 100 | parser.add_argument( 101 | "--no-deep-clone", 102 | dest="prefetch_options", 103 | action=set_argument("deep_clone", False), 104 | help="Don't include the repository history in the output derivation.", 105 | ) 106 | parser.add_argument( 107 | "--verbose", 108 | "-v", 109 | dest="logging_configuration", 110 | default=LoggingConfiguration(output_file=sys.stderr, log_level=WARNING), 111 | action=increase_log_level, 112 | help="Print additional information about the programs execution. This is useful if you want to issue a bug report.", 113 | ) 114 | parser.add_argument( 115 | "--quiet", 116 | "-q", 117 | dest="logging_configuration", 118 | action=decrease_log_level, 119 | help="Print less information about the programs execution.", 120 | ) 121 | parser.add_argument( 122 | "--nix", 123 | dest="rendering_format", 124 | default=RenderingFormat.json, 125 | action="store_const", 126 | const=RenderingFormat.nix, 127 | help="Output the results as valid nix code.", 128 | ) 129 | parser.add_argument( 130 | "--json", 131 | dest="rendering_format", 132 | action="store_const", 133 | const=RenderingFormat.json, 134 | help="Output the results in the JSON format", 135 | ) 136 | parser.add_argument( 137 | "--meta", 138 | dest="rendering_format", 139 | action="store_const", 140 | const=RenderingFormat.meta, 141 | help="Output the results in JSON format where the arguments to fetchFromGitHub are located under the src key of the resulting json dictionary and meta information about the prefetched repository is located under the meta key of the output.", 142 | ) 143 | parser.add_argument( 144 | "--version", action="version", version="%(prog)s " + VERSION_STRING 145 | ) 146 | return parser 147 | -------------------------------------------------------------------------------- /nix_prefetch_github/controller/nix_prefetch_github_controller.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from typing import List 3 | 4 | from nix_prefetch_github.controller.arguments import get_options_argument_parser 5 | from nix_prefetch_github.interfaces import GithubRepository, RenderingFormatSelector 6 | from nix_prefetch_github.logging import LoggerManager 7 | from nix_prefetch_github.use_cases.prefetch_github_repository import ( 8 | PrefetchGithubRepositoryUseCase, 9 | Request, 10 | ) 11 | 12 | 13 | class NixPrefetchGithubController: 14 | def __init__( 15 | self, 16 | use_case: PrefetchGithubRepositoryUseCase, 17 | logger_manager: LoggerManager, 18 | rendering_format_selector: RenderingFormatSelector, 19 | ) -> None: 20 | self._use_case = use_case 21 | self._logger_manager = logger_manager 22 | self._rendering_format_selector = rendering_format_selector 23 | 24 | def process_arguments(self, arguments: List[str]) -> None: 25 | parser = get_argument_parser() 26 | args = parser.parse_args(arguments) 27 | self._logger_manager.set_logging_configuration(args.logging_configuration) 28 | self._rendering_format_selector.set_rendering_format(args.rendering_format) 29 | self._use_case.prefetch_github_repository( 30 | request=Request( 31 | repository=GithubRepository(owner=args.owner, name=args.repo), 32 | revision=args.rev, 33 | prefetch_options=args.prefetch_options, 34 | ) 35 | ) 36 | 37 | 38 | # Unfortunately this needs to be a free standing function so that 39 | # sphinx-argparse can generate documentation for it. 40 | def get_argument_parser() -> argparse.ArgumentParser: 41 | parser = argparse.ArgumentParser( 42 | "nix-prefetch-github", parents=[get_options_argument_parser()] 43 | ) 44 | parser.add_argument("owner") 45 | parser.add_argument("repo") 46 | parser.add_argument("--rev", default=None) 47 | return parser 48 | -------------------------------------------------------------------------------- /nix_prefetch_github/controller/nix_prefetch_github_directory_controller.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from dataclasses import dataclass 3 | from typing import List, Protocol 4 | 5 | from nix_prefetch_github.controller.arguments import get_options_argument_parser 6 | from nix_prefetch_github.interfaces import RenderingFormatSelector 7 | from nix_prefetch_github.logging import LoggerManager 8 | from nix_prefetch_github.use_cases.prefetch_directory import ( 9 | PrefetchDirectoryUseCase, 10 | Request, 11 | ) 12 | 13 | 14 | class ProcessEnvironment(Protocol): 15 | def get_cwd(self) -> str: ... 16 | 17 | 18 | @dataclass 19 | class PrefetchDirectoryController: 20 | logger_manager: LoggerManager 21 | use_case: PrefetchDirectoryUseCase 22 | environment: ProcessEnvironment 23 | rendering_format_selector: RenderingFormatSelector 24 | 25 | def process_arguments(self, arguments: List[str]) -> None: 26 | parser = get_argument_parser() 27 | args = parser.parse_args(arguments) 28 | self.logger_manager.set_logging_configuration( 29 | configuration=args.logging_configuration 30 | ) 31 | self.rendering_format_selector.set_rendering_format(args.rendering_format) 32 | self.use_case.prefetch_directory( 33 | request=Request( 34 | prefetch_options=args.prefetch_options, 35 | directory=( 36 | args.directory if args.directory else self.environment.get_cwd() 37 | ), 38 | remote=args.remote, 39 | ) 40 | ) 41 | 42 | 43 | # Unfortunately this needs to be a free standing function so that 44 | # sphinx-argparse can generate documentation for it. 45 | def get_argument_parser() -> argparse.ArgumentParser: 46 | parser = argparse.ArgumentParser(parents=[get_options_argument_parser()]) 47 | parser.add_argument("--directory", default=None) 48 | parser.add_argument("--remote", default="origin") 49 | return parser 50 | -------------------------------------------------------------------------------- /nix_prefetch_github/controller/nix_prefetch_github_latest_release_controller.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from dataclasses import dataclass 3 | from typing import List 4 | 5 | from nix_prefetch_github.controller.arguments import get_options_argument_parser 6 | from nix_prefetch_github.interfaces import GithubRepository, RenderingFormatSelector 7 | from nix_prefetch_github.logging import LoggerManager 8 | from nix_prefetch_github.use_cases.prefetch_latest_release import ( 9 | PrefetchLatestReleaseUseCase, 10 | Request, 11 | ) 12 | 13 | 14 | @dataclass 15 | class PrefetchLatestReleaseController: 16 | use_case: PrefetchLatestReleaseUseCase 17 | logger_manager: LoggerManager 18 | rendering_format_selector: RenderingFormatSelector 19 | 20 | def process_arguments(self, arguments: List[str]) -> None: 21 | parser = get_argument_parser() 22 | args = parser.parse_args(arguments) 23 | self.logger_manager.set_logging_configuration(args.logging_configuration) 24 | self.rendering_format_selector.set_rendering_format(args.rendering_format) 25 | self.use_case.prefetch_latest_release( 26 | request=Request( 27 | repository=GithubRepository(owner=args.owner, name=args.repo), 28 | prefetch_options=args.prefetch_options, 29 | ) 30 | ) 31 | 32 | 33 | # Unfortunately this needs to be a free standing function so that 34 | # sphinx-argparse can generate documentation for it. 35 | def get_argument_parser() -> argparse.ArgumentParser: 36 | parser = argparse.ArgumentParser( 37 | "nix-prefetch-github-latest-release", 38 | parents=[get_options_argument_parser()], 39 | ) 40 | parser.add_argument("owner") 41 | parser.add_argument("repo") 42 | return parser 43 | -------------------------------------------------------------------------------- /nix_prefetch_github/controller/test_arguments.py: -------------------------------------------------------------------------------- 1 | from logging import DEBUG, ERROR, INFO, WARNING 2 | from unittest import TestCase 3 | 4 | from nix_prefetch_github.controller.arguments import get_options_argument_parser 5 | from nix_prefetch_github.interfaces import PrefetchOptions, RenderingFormat 6 | 7 | 8 | class TestGetOptionsArgumentParser(TestCase): 9 | def test_that_prefetch_options_is_present_in_namespace_when_parsing_arguments( 10 | self, 11 | ) -> None: 12 | parser = get_options_argument_parser() 13 | arguments = parser.parse_args([]) 14 | self.assertIsInstance(arguments.prefetch_options, PrefetchOptions) 15 | 16 | def test_that_fetch_submodules_can_enabled_by_specifying_appropriate_cli_option( 17 | self, 18 | ) -> None: 19 | parser = get_options_argument_parser() 20 | arguments = parser.parse_args(["--fetch-submodules"]) 21 | self.assertTrue(arguments.prefetch_options.fetch_submodules) 22 | 23 | def test_that_fetch_submodules_can_be_disabled_by_specifying_appropriate_cli_option( 24 | self, 25 | ) -> None: 26 | parser = get_options_argument_parser() 27 | arguments = parser.parse_args(["--fetch-submodules", "--no-fetch-submodules"]) 28 | self.assertFalse(arguments.prefetch_options.fetch_submodules) 29 | 30 | def test_that_by_default_leave_dot_git_is_disabled(self) -> None: 31 | parser = get_options_argument_parser() 32 | arguments = parser.parse_args([]) 33 | self.assertFalse(arguments.prefetch_options.leave_dot_git) 34 | 35 | def test_that_leave_dot_git_can_be_enabled_via_appropriate_cli_option(self) -> None: 36 | parser = get_options_argument_parser() 37 | arguments = parser.parse_args(["--leave-dot-git"]) 38 | self.assertTrue(arguments.prefetch_options.leave_dot_git) 39 | 40 | def test_that_leave_dot_git_can_be_disabled_via_appropriate_cli_option( 41 | self, 42 | ) -> None: 43 | parser = get_options_argument_parser() 44 | arguments = parser.parse_args(["--leave-dot-git", "--no-leave-dot-git"]) 45 | self.assertFalse(arguments.prefetch_options.leave_dot_git) 46 | 47 | def test_that_specifying_deep_clone_sets_prefetch_options_properly(self) -> None: 48 | parser = get_options_argument_parser() 49 | arguments = parser.parse_args(["--deep-clone"]) 50 | self.assertTrue(arguments.prefetch_options.deep_clone) 51 | 52 | def test_that_specifying_no_deep_clone_sets_prefetch_options_propery(self) -> None: 53 | parser = get_options_argument_parser() 54 | arguments = parser.parse_args(["--deep-clone", "--no-deep-clone"]) 55 | self.assertFalse(arguments.prefetch_options.deep_clone) 56 | 57 | def test_that_deep_clone_is_disabled_by_default(self) -> None: 58 | parser = get_options_argument_parser() 59 | arguments = parser.parse_args([]) 60 | self.assertFalse(arguments.prefetch_options.deep_clone) 61 | 62 | def test_that_log_level_is_WARNING_by_default(self) -> None: 63 | parser = get_options_argument_parser() 64 | arguments = parser.parse_args([]) 65 | self.assertEqual(arguments.logging_configuration.log_level, WARNING) 66 | 67 | def test_that_verbosity_flag_increases_log_level_to_INFO(self) -> None: 68 | parser = get_options_argument_parser() 69 | arguments = parser.parse_args(["--verbose"]) 70 | self.assertEqual(arguments.logging_configuration.log_level, INFO) 71 | 72 | def test_that_verbosity_flag_twice_increases_log_level_to_DEBUG(self) -> None: 73 | parser = get_options_argument_parser() 74 | arguments = parser.parse_args(["--verbose", "--verbose"]) 75 | self.assertEqual(arguments.logging_configuration.log_level, DEBUG) 76 | 77 | def test_that_quiet_decreases_log_level_to_ERROR(self) -> None: 78 | parser = get_options_argument_parser() 79 | arguments = parser.parse_args(["--quiet"]) 80 | self.assertEqual(arguments.logging_configuration.log_level, ERROR) 81 | 82 | def test_that_quiet_and_verbose_option_result_in_log_level_WARNING(self) -> None: 83 | parser = get_options_argument_parser() 84 | arguments = parser.parse_args(["-q", "-v"]) 85 | self.assertEqual(arguments.logging_configuration.log_level, WARNING) 86 | 87 | def test_rendering_option_is_json_by_default(self) -> None: 88 | parser = get_options_argument_parser() 89 | arguments = parser.parse_args([]) 90 | self.assertEqual(arguments.rendering_format, RenderingFormat.json) 91 | 92 | def test_specifying_nix_sets_rendering_option_to_nix(self) -> None: 93 | parser = get_options_argument_parser() 94 | arguments = parser.parse_args(["--nix"]) 95 | self.assertEqual(arguments.rendering_format, RenderingFormat.nix) 96 | 97 | def test_specifying_nix_and_then_json_sets_rendering_option_to_json(self) -> None: 98 | parser = get_options_argument_parser() 99 | arguments = parser.parse_args(["--nix", "--json"]) 100 | self.assertEqual(arguments.rendering_format, RenderingFormat.json) 101 | -------------------------------------------------------------------------------- /nix_prefetch_github/controller/test_nix_prefetch_github_controller.py: -------------------------------------------------------------------------------- 1 | from logging import INFO 2 | from typing import Optional 3 | from unittest import TestCase 4 | 5 | from nix_prefetch_github.controller.nix_prefetch_github_controller import ( 6 | NixPrefetchGithubController, 7 | ) 8 | from nix_prefetch_github.interfaces import ( 9 | GithubRepository, 10 | PrefetchOptions, 11 | RenderingFormat, 12 | ) 13 | from nix_prefetch_github.tests import FakeLoggerManager, RenderingFormatSelectorImpl 14 | from nix_prefetch_github.use_cases.prefetch_github_repository import Request 15 | 16 | 17 | class ControllerTests(TestCase): 18 | def setUp(self) -> None: 19 | self.logger_manager = FakeLoggerManager() 20 | self.rendering_format_selector = RenderingFormatSelectorImpl() 21 | self.use_case_mock = UseCaseImpl() 22 | self.controller = NixPrefetchGithubController( 23 | use_case=self.use_case_mock, 24 | logger_manager=self.logger_manager, 25 | rendering_format_selector=self.rendering_format_selector, 26 | ) 27 | 28 | def test_controller_extracts_example_owner_and_repo_from_arguments(self) -> None: 29 | expected_owner = "owner" 30 | expected_repo = "repo" 31 | self.controller.process_arguments([expected_owner, expected_repo]) 32 | self.assertRepository( 33 | GithubRepository(owner=expected_owner, name=expected_repo), 34 | ) 35 | 36 | def test_controller_extracts_alternative_owner_and_repo_from_arguments( 37 | self, 38 | ) -> None: 39 | expected_owner = "other_owner" 40 | expected_repo = "other_repo" 41 | self.controller.process_arguments([expected_owner, expected_repo]) 42 | self.assertRepository( 43 | GithubRepository(owner=expected_owner, name=expected_repo), 44 | ) 45 | 46 | def test_controller_can_select_nix_renderer_when_argument_is_specified( 47 | self, 48 | ) -> None: 49 | self.controller.process_arguments(["owner", "repo", "--nix"]) 50 | self.assertRenderingFormat(RenderingFormat.nix) 51 | 52 | def test_controller_can_select_json_renderer_when_argument_is_specified( 53 | self, 54 | ) -> None: 55 | self.controller.process_arguments(["owner", "repo", "--json"]) 56 | self.assertRenderingFormat(RenderingFormat.json) 57 | 58 | def test_controller_chooses_json_renderer_by_default(self) -> None: 59 | self.controller.process_arguments(["owner", "repo"]) 60 | self.assertRenderingFormat(RenderingFormat.json) 61 | 62 | def test_controller_can_handle_rendering_flag_in_front_of_arguments(self) -> None: 63 | expected_owner = "owner" 64 | expected_repo = "repo" 65 | self.controller.process_arguments(["--nix", expected_owner, expected_repo]) 66 | self.assertRepository( 67 | GithubRepository(owner=expected_owner, name=expected_repo), 68 | ) 69 | 70 | def test_can_extract_revision_from_arguments(self) -> None: 71 | expected_revision = "test rev" 72 | self.controller.process_arguments(["owner", "repo", "--rev", expected_revision]) 73 | self.assertRevision(expected_revision) 74 | 75 | def test_can_extract_an_alternative_revision_from_arguments(self) -> None: 76 | expected_revision = "alternative revision" 77 | self.controller.process_arguments(["owner", "repo", "--rev", expected_revision]) 78 | self.assertRevision(expected_revision) 79 | 80 | def test_extract_deep_clone_request_from_arguments(self) -> None: 81 | self.controller.process_arguments(["owner", "repo", "--deep-clone"]) 82 | self.assertPrefetchOptions(PrefetchOptions(deep_clone=True)) 83 | 84 | def test_extract_non_deep_clone_request_from_arguments(self) -> None: 85 | self.controller.process_arguments(["owner", "repo", "--no-deep-clone"]) 86 | self.assertPrefetchOptions(PrefetchOptions(deep_clone=False)) 87 | 88 | def test_deep_clone_is_false_by_default(self) -> None: 89 | self.controller.process_arguments(["owner", "repo"]) 90 | self.assertPrefetchOptions(PrefetchOptions(deep_clone=False)) 91 | 92 | def test_extact_leave_dot_git_from_arguments(self) -> None: 93 | self.controller.process_arguments(["owner", "repo", "--leave-dot-git"]) 94 | self.assertPrefetchOptions(PrefetchOptions(leave_dot_git=True)) 95 | 96 | def test_extract_no_leave_dot_git_from_arguments(self) -> None: 97 | self.controller.process_arguments(["owner", "repo", "--no-leave-dot-git"]) 98 | self.assertPrefetchOptions(PrefetchOptions(leave_dot_git=False)) 99 | 100 | def test_extract_fetch_submodules_from_arguments(self) -> None: 101 | self.controller.process_arguments(["owner", "repo", "--fetch-submodules"]) 102 | self.assertPrefetchOptions(PrefetchOptions(fetch_submodules=True)) 103 | 104 | def test_extract_no_fetch_submodules_from_arguments(self) -> None: 105 | self.controller.process_arguments(["owner", "repo", "--no-fetch-submodules"]) 106 | self.assertPrefetchOptions(PrefetchOptions(fetch_submodules=False)) 107 | 108 | def test_can_set_log_level_with_arguments(self) -> None: 109 | self.controller.process_arguments(["owner", "repo", "-v"]) 110 | self.logger_manager.assertLoggingConfiguration(lambda c: c.log_level == INFO) 111 | 112 | def assertPrefetchOptions(self, prefetch_options: PrefetchOptions) -> None: 113 | assert self.use_case_mock.request 114 | self.assertEqual( 115 | self.use_case_mock.request.prefetch_options, 116 | prefetch_options, 117 | ) 118 | 119 | def assertRevision(self, revision: Optional[str]) -> None: 120 | assert self.use_case_mock.request 121 | self.assertEqual( 122 | self.use_case_mock.request.revision, 123 | revision, 124 | ) 125 | 126 | def assertRepository(self, repository: GithubRepository) -> None: 127 | assert self.use_case_mock.request 128 | self.assertEqual( 129 | self.use_case_mock.request.repository, 130 | repository, 131 | ) 132 | 133 | def assertRenderingFormat(self, rendering_format: RenderingFormat) -> None: 134 | self.assertEqual( 135 | self.rendering_format_selector.selected_output_format, rendering_format 136 | ) 137 | 138 | 139 | class UseCaseImpl: 140 | def __init__(self) -> None: 141 | self.request: Optional[Request] = None 142 | 143 | def prefetch_github_repository(self, request: Request) -> None: 144 | assert not self.request 145 | self.request = request 146 | -------------------------------------------------------------------------------- /nix_prefetch_github/controller/test_nix_prefetch_github_directory_controller.py: -------------------------------------------------------------------------------- 1 | from logging import INFO, WARNING 2 | from typing import List 3 | from unittest import TestCase 4 | 5 | from nix_prefetch_github.interfaces import RenderingFormat 6 | from nix_prefetch_github.tests import FakeLoggerManager, RenderingFormatSelectorImpl 7 | from nix_prefetch_github.use_cases.prefetch_directory import Request 8 | 9 | from .nix_prefetch_github_directory_controller import PrefetchDirectoryController 10 | 11 | 12 | class ControllerTests(TestCase): 13 | def setUp(self) -> None: 14 | self.logger_manager = FakeLoggerManager() 15 | self.fake_use_case = FakeUseCase() 16 | self.environment = FakeEnvironment() 17 | self.rendering_format_selector = RenderingFormatSelectorImpl() 18 | self.controller = PrefetchDirectoryController( 19 | logger_manager=self.logger_manager, 20 | use_case=self.fake_use_case, 21 | environment=self.environment, 22 | rendering_format_selector=self.rendering_format_selector, 23 | ) 24 | 25 | def test_can_set_logging_configuration_via_argument(self) -> None: 26 | self.controller.process_arguments(["-v"]) 27 | self.logger_manager.assertLoggingConfiguration(lambda c: c.log_level == INFO) 28 | 29 | def test_log_level_is_warning_by_default(self) -> None: 30 | self.controller.process_arguments([]) 31 | self.logger_manager.assertLoggingConfiguration(lambda c: c.log_level == WARNING) 32 | 33 | def test_that_by_default_directory_is_detected_from_environment(self) -> None: 34 | expected_directory = "testdir" 35 | self.environment.set_cwd(expected_directory) 36 | self.controller.process_arguments([]) 37 | self.assertEqual( 38 | self.fake_use_case.requests[-1].directory, 39 | expected_directory, 40 | ) 41 | 42 | def test_can_parse_directory_from_arguments(self) -> None: 43 | expected_directory = "test/directory" 44 | self.controller.process_arguments(["--directory", expected_directory]) 45 | self.assertEqual( 46 | self.fake_use_case.requests[-1].directory, 47 | expected_directory, 48 | ) 49 | 50 | def test_use_cwd_if_directory_passed_is_empty_string(self) -> None: 51 | expected_directory = "test/directory" 52 | self.environment.set_cwd(expected_directory) 53 | self.controller.process_arguments(["--directory", ""]) 54 | self.assertEqual( 55 | self.fake_use_case.requests[-1].directory, 56 | expected_directory, 57 | ) 58 | 59 | def test_remote_is_origin_by_default(self) -> None: 60 | self.controller.process_arguments([]) 61 | self.assertEqual( 62 | self.fake_use_case.requests[-1].remote, 63 | "origin", 64 | ) 65 | 66 | def test_can_specify_origin_via_arguments(self) -> None: 67 | expected_origin = "upstream" 68 | self.controller.process_arguments(["--remote", expected_origin]) 69 | self.assertEqual( 70 | self.fake_use_case.requests[-1].remote, 71 | expected_origin, 72 | ) 73 | 74 | def test_that_json_rendering_format_is_seleted_when_json_is_given_as_argument( 75 | self, 76 | ) -> None: 77 | self.controller.process_arguments(["--json"]) 78 | self.assertEqual( 79 | self.rendering_format_selector.selected_output_format, 80 | RenderingFormat.json, 81 | ) 82 | 83 | def test_that_nix_rendering_format_is_seleted_when_nix_is_given_as_argument( 84 | self, 85 | ) -> None: 86 | self.controller.process_arguments(["--nix"]) 87 | self.assertEqual( 88 | self.rendering_format_selector.selected_output_format, 89 | RenderingFormat.nix, 90 | ) 91 | 92 | def test_can_specify_deep_clone_via_arguments(self) -> None: 93 | self.controller.process_arguments(["--deep-clone"]) 94 | self.assertTrue( 95 | self.fake_use_case.requests[-1].prefetch_options.deep_clone, 96 | ) 97 | 98 | 99 | class FakeUseCase: 100 | def __init__(self) -> None: 101 | self.requests: List[Request] = [] 102 | 103 | def prefetch_directory(self, request: Request) -> None: 104 | self.requests.append(request) 105 | 106 | 107 | class FakeEnvironment: 108 | def __init__(self) -> None: 109 | self.cwd = "unset/cwd" 110 | 111 | def set_cwd(self, path: str) -> None: 112 | self.cwd = path 113 | 114 | def get_cwd(self) -> str: 115 | return self.cwd 116 | -------------------------------------------------------------------------------- /nix_prefetch_github/controller/test_nix_prefetch_github_latest_release_controller.py: -------------------------------------------------------------------------------- 1 | from logging import INFO, WARNING 2 | from typing import Callable, Optional, cast 3 | from unittest import TestCase 4 | 5 | from nix_prefetch_github.controller.nix_prefetch_github_latest_release_controller import ( 6 | PrefetchLatestReleaseController, 7 | ) 8 | from nix_prefetch_github.interfaces import RenderingFormat 9 | from nix_prefetch_github.tests import FakeLoggerManager, RenderingFormatSelectorImpl 10 | from nix_prefetch_github.use_cases.prefetch_latest_release import Request 11 | 12 | 13 | class ControllerTests(TestCase): 14 | def setUp(self) -> None: 15 | self.logger_manager = FakeLoggerManager() 16 | self.rendering_format_selector = RenderingFormatSelectorImpl() 17 | self.fake_use_case = FakeUseCase() 18 | self.controller = PrefetchLatestReleaseController( 19 | use_case=self.fake_use_case, 20 | logger_manager=self.logger_manager, 21 | rendering_format_selector=self.rendering_format_selector, 22 | ) 23 | 24 | def test_that_correct_owner_is_detected_from_arguments(self) -> None: 25 | self.controller.process_arguments(["owner", "repo"]) 26 | self.assertRequest( 27 | lambda r: r.repository.owner == "owner", 28 | message="Requested repository has wrong owner", 29 | ) 30 | 31 | def test_that_correct_repo_name_is_detected_from_arguments(self) -> None: 32 | self.controller.process_arguments(["owner", "repo"]) 33 | self.assertRequest( 34 | lambda r: r.repository.name == "repo", 35 | message="Requested repository has wrong name", 36 | ) 37 | 38 | def test_log_level_is_detected_from_arguments(self) -> None: 39 | self.controller.process_arguments(["owner", "repo", "-v"]) 40 | self.logger_manager.assertLoggingConfiguration(lambda c: c.log_level == INFO) 41 | 42 | def test_by_default_log_level_is_set_to_warning(self) -> None: 43 | self.controller.process_arguments(["owner", "repo"]) 44 | self.logger_manager.assertLoggingConfiguration(lambda c: c.log_level == WARNING) 45 | 46 | def test_that_json_rendering_format_is_seleted_when_json_is_given_as_argument( 47 | self, 48 | ) -> None: 49 | self.controller.process_arguments(["owner", "repo", "--json"]) 50 | self.assertEqual( 51 | self.rendering_format_selector.selected_output_format, 52 | RenderingFormat.json, 53 | ) 54 | 55 | def test_that_nix_rendering_format_is_seleted_when_nix_is_given_as_argument( 56 | self, 57 | ) -> None: 58 | self.controller.process_arguments(["owner", "repo", "--nix"]) 59 | self.assertEqual( 60 | self.rendering_format_selector.selected_output_format, 61 | RenderingFormat.nix, 62 | ) 63 | 64 | def assertRequest( 65 | self, 66 | condition: Optional[Callable[[Request], bool]] = None, 67 | message: str = "", 68 | ) -> None: 69 | self.assertIsNotNone(self.fake_use_case.request) 70 | if condition: 71 | self.assertTrue( 72 | condition(cast(Request, self.fake_use_case.request)), msg=message 73 | ) 74 | 75 | 76 | class FakeUseCase: 77 | def __init__(self) -> None: 78 | self.request: Optional[Request] = None 79 | 80 | def prefetch_latest_release(self, request: Request) -> None: 81 | assert not self.request 82 | self.request = request 83 | -------------------------------------------------------------------------------- /nix_prefetch_github/dependency_injector.py: -------------------------------------------------------------------------------- 1 | import os 2 | from functools import lru_cache 3 | from logging import Logger 4 | 5 | from nix_prefetch_github.alerter import CliAlerterImpl 6 | from nix_prefetch_github.command.command_runner import CommandRunnerImpl 7 | from nix_prefetch_github.controller.nix_prefetch_github_controller import ( 8 | NixPrefetchGithubController, 9 | ) 10 | from nix_prefetch_github.controller.nix_prefetch_github_directory_controller import ( 11 | PrefetchDirectoryController, 12 | ) 13 | from nix_prefetch_github.controller.nix_prefetch_github_latest_release_controller import ( 14 | PrefetchLatestReleaseController, 15 | ) 16 | from nix_prefetch_github.github import GithubAPIImpl 17 | from nix_prefetch_github.hash_converter import HashConverterImpl 18 | from nix_prefetch_github.interfaces import ( 19 | GithubAPI, 20 | RepositoryDetector, 21 | RevisionIndexFactory, 22 | ) 23 | from nix_prefetch_github.list_remote_factory import ListRemoteFactoryImpl 24 | from nix_prefetch_github.logging import LoggerFactoryImpl 25 | from nix_prefetch_github.prefetch import PrefetcherImpl 26 | from nix_prefetch_github.presenter import PresenterImpl 27 | from nix_prefetch_github.presenter.repository_renderer import ( 28 | JsonRepositoryRenderer, 29 | MetaRepositoryRenderer, 30 | NixRepositoryRenderer, 31 | RenderingSelectorImpl, 32 | ) 33 | from nix_prefetch_github.process_environment import ProcessEnvironmentImpl 34 | from nix_prefetch_github.repository_detector import RepositoryDetectorImpl 35 | from nix_prefetch_github.revision_index_factory import RevisionIndexFactoryImpl 36 | from nix_prefetch_github.url_hasher.nix_prefetch import NixPrefetchUrlHasherImpl 37 | from nix_prefetch_github.use_cases.prefetch_directory import ( 38 | PrefetchDirectoryUseCaseImpl, 39 | ) 40 | from nix_prefetch_github.use_cases.prefetch_github_repository import ( 41 | PrefetchGithubRepositoryUseCaseImpl, 42 | ) 43 | from nix_prefetch_github.use_cases.prefetch_latest_release import ( 44 | PrefetchLatestReleaseUseCaseImpl, 45 | ) 46 | from nix_prefetch_github.views import CommandLineViewImpl 47 | 48 | 49 | class DependencyInjector: 50 | def get_alerter(self) -> CliAlerterImpl: 51 | return CliAlerterImpl( 52 | logger=self.get_logger(), 53 | ) 54 | 55 | def get_revision_index_factory(self) -> RevisionIndexFactory: 56 | return RevisionIndexFactoryImpl(self.get_remote_list_factory()) 57 | 58 | def get_process_environment(self) -> ProcessEnvironmentImpl: 59 | return ProcessEnvironmentImpl() 60 | 61 | def get_remote_list_factory(self) -> ListRemoteFactoryImpl: 62 | return ListRemoteFactoryImpl(command_runner=self.get_command_runner()) 63 | 64 | def get_view(self) -> CommandLineViewImpl: 65 | return CommandLineViewImpl() 66 | 67 | def get_nix_prefetch_url_hasher_impl(self) -> NixPrefetchUrlHasherImpl: 68 | return NixPrefetchUrlHasherImpl( 69 | command_runner=self.get_command_runner(), 70 | logger=self.get_logger(), 71 | hash_converter=self.get_hash_converter(), 72 | ) 73 | 74 | def get_hash_converter(self) -> HashConverterImpl: 75 | return HashConverterImpl(command_runner=self.get_command_runner()) 76 | 77 | def get_prefetcher(self) -> PrefetcherImpl: 78 | return PrefetcherImpl( 79 | self.get_nix_prefetch_url_hasher_impl(), self.get_revision_index_factory() 80 | ) 81 | 82 | def get_nix_repository_renderer(self) -> NixRepositoryRenderer: 83 | return NixRepositoryRenderer() 84 | 85 | def get_json_repository_renderer(self) -> JsonRepositoryRenderer: 86 | return JsonRepositoryRenderer() 87 | 88 | def get_meta_repository_renderer(self) -> MetaRepositoryRenderer: 89 | return MetaRepositoryRenderer( 90 | github_api=self.get_github_api(), 91 | json_renderer=self.get_json_repository_renderer(), 92 | ) 93 | 94 | @lru_cache 95 | def get_rendering_format_selector(self) -> RenderingSelectorImpl: 96 | return RenderingSelectorImpl( 97 | nix_renderer=self.get_nix_repository_renderer(), 98 | json_renderer=self.get_json_repository_renderer(), 99 | meta_renderer=self.get_meta_repository_renderer(), 100 | ) 101 | 102 | def get_github_api(self) -> GithubAPI: 103 | return GithubAPIImpl(logger=self.get_logger(), environment=os.environ) 104 | 105 | def get_repository_detector(self) -> RepositoryDetector: 106 | return RepositoryDetectorImpl( 107 | command_runner=self.get_command_runner(), logger=self.get_logger() 108 | ) 109 | 110 | def get_command_runner(self) -> CommandRunnerImpl: 111 | return CommandRunnerImpl(logger=self.get_logger()) 112 | 113 | @lru_cache() 114 | def get_logger_factory(self) -> LoggerFactoryImpl: 115 | return LoggerFactoryImpl() 116 | 117 | def get_logger(self) -> Logger: 118 | factory = self.get_logger_factory() 119 | return factory.get_logger() 120 | 121 | def get_presenter_impl(self) -> PresenterImpl: 122 | return PresenterImpl( 123 | view=self.get_view(), 124 | repository_renderer=self.get_rendering_format_selector(), 125 | ) 126 | 127 | def get_prefetch_latest_release_use_case(self) -> PrefetchLatestReleaseUseCaseImpl: 128 | return PrefetchLatestReleaseUseCaseImpl( 129 | presenter=self.get_presenter_impl(), 130 | prefetcher=self.get_prefetcher(), 131 | github_api=self.get_github_api(), 132 | ) 133 | 134 | def get_prefetch_github_repository_use_case( 135 | self, 136 | ) -> PrefetchGithubRepositoryUseCaseImpl: 137 | return PrefetchGithubRepositoryUseCaseImpl( 138 | presenter=self.get_presenter_impl(), 139 | prefetcher=self.get_prefetcher(), 140 | alerter=self.get_alerter(), 141 | ) 142 | 143 | def get_prefetch_directory_use_case(self) -> PrefetchDirectoryUseCaseImpl: 144 | return PrefetchDirectoryUseCaseImpl( 145 | presenter=self.get_presenter_impl(), 146 | prefetcher=self.get_prefetcher(), 147 | repository_detector=self.get_repository_detector(), 148 | logger=self.get_logger(), 149 | ) 150 | 151 | def get_prefetch_github_repository_controller(self) -> NixPrefetchGithubController: 152 | return NixPrefetchGithubController( 153 | use_case=self.get_prefetch_github_repository_use_case(), 154 | logger_manager=self.get_logger_factory(), 155 | rendering_format_selector=self.get_rendering_format_selector(), 156 | ) 157 | 158 | def get_prefetch_latest_release_controller(self) -> PrefetchLatestReleaseController: 159 | return PrefetchLatestReleaseController( 160 | use_case=self.get_prefetch_latest_release_use_case(), 161 | logger_manager=self.get_logger_factory(), 162 | rendering_format_selector=self.get_rendering_format_selector(), 163 | ) 164 | 165 | def get_prefetch_directory_controller(self) -> PrefetchDirectoryController: 166 | return PrefetchDirectoryController( 167 | logger_manager=self.get_logger_factory(), 168 | use_case=self.get_prefetch_directory_use_case(), 169 | environment=self.get_process_environment(), 170 | rendering_format_selector=self.get_rendering_format_selector(), 171 | ) 172 | -------------------------------------------------------------------------------- /nix_prefetch_github/functor.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Optional, TypeVar 2 | 3 | T = TypeVar("T") 4 | U = TypeVar("U") 5 | 6 | 7 | def map_or_none(mapping: Callable[[T], U], value: Optional[T]) -> Optional[U]: 8 | if value is None: 9 | return None 10 | else: 11 | return mapping(value) 12 | -------------------------------------------------------------------------------- /nix_prefetch_github/github.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | from datetime import datetime 4 | from http.client import HTTPResponse 5 | from json.decoder import JSONDecodeError 6 | from logging import Logger 7 | from typing import Any, Optional, Protocol 8 | from urllib.error import HTTPError 9 | from urllib.request import Request, urlopen 10 | 11 | from nix_prefetch_github.interfaces import GithubRepository 12 | 13 | 14 | class Environment(Protocol): 15 | def get(self, key: str) -> Optional[str]: ... 16 | 17 | 18 | class GithubAPIImpl: 19 | def __init__(self, logger: Logger, environment: Environment) -> None: 20 | self.logger = logger 21 | self._environment = environment 22 | 23 | def get_tag_of_latest_release(self, repository: GithubRepository) -> Optional[str]: 24 | self.logger.info( 25 | f"Query latest release for repository {repository.owner}/{repository.name} from GitHub." 26 | ) 27 | url = f"https://api.github.com/repos/{repository.owner}/{repository.name}/releases/latest" 28 | response_json = self._request_json_document(url) 29 | if response_json is None: 30 | return None 31 | return response_json.get("tag_name") 32 | 33 | def get_commit_date( 34 | self, repository: GithubRepository, commit_sha1_hash: str 35 | ) -> Optional[datetime]: 36 | url = f"https://api.github.com/repos/{repository.owner}/{repository.name}/commits/{commit_sha1_hash}" 37 | response_json = self._request_json_document(url) 38 | if response_json is None: 39 | return None 40 | date_string = response_json.get("commit", {}).get("committer", {}).get("date") 41 | return self._parse_timestamp(date_string) 42 | 43 | def _parse_timestamp(self, timestamp: str) -> Optional[datetime]: 44 | try: 45 | return datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S%z") 46 | except ValueError as e: 47 | self.logger.exception(e) 48 | return None 49 | 50 | def _request_json_document(self, url: str) -> Optional[Any]: 51 | self.logger.debug("GET JSON document from %s", url) 52 | request = Request(url) 53 | environment_variable_name = "GITHUB_TOKEN" 54 | if api_key := self._environment.get(environment_variable_name): 55 | self.logger.debug( 56 | "Authenticating via GitHub API token from environment variable '%s'", 57 | environment_variable_name, 58 | ) 59 | request.add_header("Authorization", f"Bearer {api_key}") 60 | try: 61 | with urlopen(request) as response: 62 | self.logger.debug( 63 | "Response was %(status)s %(reason)s", 64 | dict(status=response.status, reason=response.reason), 65 | ) 66 | return self._decode_json_from_response(response) 67 | except (HTTPError, JSONDecodeError) as e: 68 | self.logger.error(e) 69 | return None 70 | 71 | def _decode_json_from_response(self, response: HTTPResponse) -> Any: 72 | return json.load( 73 | io.TextIOWrapper( 74 | response, encoding=response.info().get_content_charset("utf-8") 75 | ) 76 | ) 77 | -------------------------------------------------------------------------------- /nix_prefetch_github/hash.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from dataclasses import dataclass 5 | from typing import List 6 | 7 | 8 | @dataclass 9 | class SriHash: 10 | hash_function: str 11 | digest: str 12 | options: List[str] 13 | 14 | @classmethod 15 | def from_text(cls, text: str) -> SriHash: 16 | hash_function, remainder = text.split("-", maxsplit=1) 17 | digest_and_options = remainder.split("?") 18 | digest = digest_and_options[0] 19 | return cls( 20 | hash_function=hash_function, 21 | digest=digest, 22 | options=digest_and_options[1:], 23 | ) 24 | 25 | def __str__(self) -> str: 26 | options = ["?" + option for option in self.options] 27 | return f"{self.hash_function}-{self.digest}" + "".join(options) 28 | 29 | 30 | def is_sha1_hash(text: str) -> bool: 31 | return bool(re.match(r"^[0-9a-f]{40}$", text)) 32 | -------------------------------------------------------------------------------- /nix_prefetch_github/hash_converter.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | from nix_prefetch_github.interfaces import CommandRunner 5 | 6 | 7 | @dataclass 8 | class HashConverterImpl: 9 | command_runner: CommandRunner 10 | 11 | def convert_sha256_to_sri(self, original: str) -> Optional[str]: 12 | returncode, output = self.command_runner.run_command( 13 | [ 14 | "nix", 15 | "--extra-experimental-features", 16 | "nix-command", 17 | "hash", 18 | "to-sri", 19 | f"sha256:{original}", 20 | ], 21 | ) 22 | if not returncode: 23 | return output.strip() 24 | return None 25 | -------------------------------------------------------------------------------- /nix_prefetch_github/interfaces.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import enum 4 | from dataclasses import dataclass 5 | from datetime import datetime 6 | from typing import Dict, List, Optional, Protocol, Tuple, Union 7 | 8 | 9 | class Alerter(Protocol): 10 | def alert_user_about_unsafe_prefetch_options( 11 | self, prefetch_options: PrefetchOptions 12 | ) -> None: ... 13 | 14 | 15 | class RevisionIndex(Protocol): 16 | def get_revision_by_name(self, name: str) -> Optional[str]: ... 17 | 18 | 19 | @dataclass(frozen=True) 20 | class GithubRepository: 21 | owner: str 22 | name: str 23 | 24 | def url(self) -> str: 25 | return f"https://github.com/{self.owner}/{self.name}.git" 26 | 27 | 28 | @dataclass 29 | class PrefetchOptions: 30 | fetch_submodules: bool = False 31 | deep_clone: bool = False 32 | leave_dot_git: bool = False 33 | 34 | def is_safe(self) -> bool: 35 | return not self.deep_clone and not self.leave_dot_git 36 | 37 | 38 | class HashConverter(Protocol): 39 | def convert_sha256_to_sri(self, original: str) -> Optional[str]: ... 40 | 41 | 42 | @dataclass 43 | class PrefetchedRessource: 44 | hash_sum: str 45 | store_path: str 46 | 47 | 48 | class UrlHasher(Protocol): 49 | def calculate_hash_sum( 50 | self, 51 | repository: GithubRepository, 52 | revision: str, 53 | prefetch_options: PrefetchOptions, 54 | ) -> Optional[PrefetchedRessource]: ... 55 | 56 | 57 | class GithubAPI(Protocol): 58 | def get_tag_of_latest_release( 59 | self, repository: GithubRepository 60 | ) -> Optional[str]: ... 61 | 62 | def get_commit_date( 63 | self, repository: GithubRepository, commit_sha1_hash: str 64 | ) -> Optional[datetime]: ... 65 | 66 | 67 | class RevisionIndexFactory(Protocol): 68 | def get_revision_index( 69 | self, repository: GithubRepository 70 | ) -> Optional[RevisionIndex]: ... 71 | 72 | 73 | class RepositoryDetector(Protocol): 74 | def detect_github_repository( 75 | self, directory: str, remote_name: Optional[str] 76 | ) -> Optional[GithubRepository]: ... 77 | 78 | def is_repository_dirty(self, directory: str) -> bool: ... 79 | 80 | def get_current_revision(self, directory: str) -> Optional[str]: ... 81 | 82 | 83 | @dataclass(frozen=True) 84 | class PrefetchedRepository: 85 | repository: GithubRepository 86 | rev: str 87 | hash_sum: str 88 | options: PrefetchOptions 89 | store_path: str 90 | 91 | 92 | @dataclass 93 | class PrefetchFailure: 94 | class Reason(enum.Enum): 95 | unable_to_locate_revision = enum.auto() 96 | unable_to_calculate_hash_sum = enum.auto() 97 | 98 | def __str__(self) -> str: 99 | if self == self.unable_to_calculate_hash_sum: 100 | return "Unable to calculate hash sum" 101 | else: 102 | return "Unable to locate revision" 103 | 104 | reason: Reason 105 | 106 | 107 | PrefetchResult = Union[PrefetchedRepository, PrefetchFailure] 108 | 109 | 110 | class Prefetcher(Protocol): 111 | def prefetch_github( 112 | self, 113 | repository: GithubRepository, 114 | rev: Optional[str], 115 | prefetch_options: PrefetchOptions, 116 | ) -> PrefetchResult: ... 117 | 118 | 119 | class CommandRunner(Protocol): 120 | def run_command( 121 | self, 122 | command: List[str], 123 | cwd: Optional[str] = None, 124 | environment_variables: Optional[Dict[str, str]] = None, 125 | merge_stderr: bool = False, 126 | ) -> Tuple[int, str]: ... 127 | 128 | 129 | class RepositoryRenderer(Protocol): 130 | def render_prefetched_repository(self, repository: PrefetchedRepository) -> str: ... 131 | 132 | 133 | class Presenter(Protocol): 134 | def present(self, prefetch_result: PrefetchResult) -> None: ... 135 | 136 | 137 | @enum.unique 138 | class RenderingFormat(enum.Enum): 139 | nix = enum.auto() 140 | json = enum.auto() 141 | meta = enum.auto() 142 | 143 | 144 | class RenderingFormatSelector(Protocol): 145 | def set_rendering_format(self, rendering_format: RenderingFormat) -> None: ... 146 | 147 | 148 | @dataclass 149 | class ViewModel: 150 | exit_code: int 151 | stderr_lines: List[str] 152 | stdout_lines: List[str] 153 | 154 | 155 | class CommandLineView(Protocol): 156 | def render_view_model(self, model: ViewModel) -> None: ... 157 | -------------------------------------------------------------------------------- /nix_prefetch_github/list_remote.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import Enum, unique 4 | from typing import Dict, Optional 5 | 6 | 7 | @unique 8 | class RefKind(Enum): 9 | Head = 1 10 | Tag = 2 11 | 12 | 13 | class ListRemote: 14 | def __init__( 15 | self, 16 | symrefs: Dict[str, str] = dict(), 17 | heads: Dict[str, str] = dict(), 18 | tags: Dict[str, str] = dict(), 19 | ) -> None: 20 | self.heads = heads 21 | self.symrefs = symrefs 22 | self.tags = tags 23 | 24 | @classmethod 25 | def from_git_ls_remote_output(constructor, output: str) -> ListRemote: 26 | builder = _Builder() 27 | for line in output.splitlines(): 28 | builder.parse_line(line) 29 | return builder.to_list_remote() 30 | 31 | def branch(self, branch_name: str) -> Optional[str]: 32 | return self.heads.get(branch_name) 33 | 34 | def symref(self, ref_name: str) -> Optional[str]: 35 | return self.symrefs.get(ref_name) 36 | 37 | def tag(self, tag_name: str) -> Optional[str]: 38 | return self.tags.get(tag_name) 39 | 40 | def full_ref_name(self, ref_name: str) -> Optional[str]: 41 | try: 42 | kind = kind_from_ref(ref_name) 43 | except ValueError: 44 | return None 45 | name = name_from_ref(ref_name) 46 | if not name: 47 | return None 48 | if kind == RefKind.Tag: 49 | return self.tag(name) 50 | elif kind == RefKind.Head: 51 | return self.branch(name) 52 | else: 53 | return None 54 | 55 | 56 | class _Builder: 57 | def __init__(self) -> None: 58 | self.symrefs: Dict[str, str] = dict() 59 | self.heads: Dict[str, str] = dict() 60 | self.tags: Dict[str, str] = dict() 61 | 62 | def parse_line(self, line: str) -> None: 63 | try: 64 | prefix, suffix = line.split("\t") 65 | except ValueError: 66 | return 67 | if line.startswith("ref: "): 68 | ref = prefix[5:] 69 | branch_name = name_from_ref(ref) 70 | if branch_name: 71 | self.symrefs[suffix] = branch_name 72 | else: 73 | try: 74 | kind = kind_from_ref(suffix) 75 | except ValueError: 76 | return 77 | name = name_from_ref(suffix) 78 | if name: 79 | if kind == RefKind.Head: 80 | self.heads[name] = prefix 81 | elif kind == RefKind.Tag: 82 | self.tags[name] = prefix 83 | 84 | def to_list_remote(self) -> ListRemote: 85 | return ListRemote(symrefs=self.symrefs, heads=self.heads, tags=self.tags) 86 | 87 | 88 | def name_from_ref(ref: str) -> Optional[str]: 89 | fragments = ref.split("/") 90 | # the first two fragments are exprected to be "refs" and 91 | # "heads", after that the proper ref name should appear 92 | return "/".join(fragments[2:]) or None 93 | 94 | 95 | def kind_from_ref(ref: str) -> RefKind: 96 | fragments = ref.split("/") 97 | try: 98 | kind = fragments[1] 99 | except IndexError: 100 | raise ValueError( 101 | f"`{ref}` does not look like a proper entry from `git ls-remote`" 102 | ) 103 | if kind == "heads": 104 | return RefKind.Head 105 | elif kind == "tags": 106 | return RefKind.Tag 107 | else: 108 | raise ValueError(f"Ref kind not recognized: `{kind}`") 109 | -------------------------------------------------------------------------------- /nix_prefetch_github/list_remote_factory.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | from nix_prefetch_github.interfaces import CommandRunner, GithubRepository 5 | from nix_prefetch_github.list_remote import ListRemote 6 | 7 | 8 | @dataclass(frozen=True) 9 | class ListRemoteFactoryImpl: 10 | command_runner: CommandRunner 11 | 12 | def get_list_remote(self, repository: GithubRepository) -> Optional[ListRemote]: 13 | repository_url = repository.url() 14 | returncode, output = self.command_runner.run_command( 15 | command=["git", "ls-remote", "--symref", repository_url], 16 | environment_variables={"GIT_ASKPASS": "", "GIT_TERMINAL_PROMPT": "0"}, 17 | merge_stderr=False, 18 | ) 19 | if returncode == 0: 20 | return ListRemote.from_git_ls_remote_output(output) 21 | else: 22 | return None 23 | -------------------------------------------------------------------------------- /nix_prefetch_github/logging.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from logging import ( 5 | DEBUG, 6 | ERROR, 7 | INFO, 8 | WARNING, 9 | Formatter, 10 | Logger, 11 | StreamHandler, 12 | getLogger, 13 | ) 14 | from typing import Protocol, TextIO 15 | 16 | 17 | @dataclass 18 | class LoggingConfiguration: 19 | output_file: TextIO 20 | log_level: int = WARNING 21 | 22 | def increase_log_level(self) -> None: 23 | if self.log_level == INFO: 24 | new_level = DEBUG 25 | elif self.log_level == WARNING: 26 | new_level = INFO 27 | else: 28 | new_level = WARNING 29 | self.log_level = new_level 30 | 31 | def decrease_log_level(self) -> None: 32 | if self.log_level == DEBUG: 33 | new_level = INFO 34 | elif self.log_level == INFO: 35 | new_level = WARNING 36 | else: 37 | new_level = ERROR 38 | self.log_level = new_level 39 | 40 | 41 | class LoggerManager(Protocol): 42 | def set_logging_configuration( 43 | self, configuration: LoggingConfiguration 44 | ) -> None: ... 45 | 46 | 47 | class LoggerFactoryImpl: 48 | def __init__(self) -> None: 49 | self._logger = getLogger("") 50 | 51 | def get_logger(self) -> Logger: 52 | return self._logger 53 | 54 | def set_logging_configuration(self, configuration: LoggingConfiguration) -> None: 55 | self._apply_configuration_to_logger(self._logger, configuration) 56 | 57 | def _apply_configuration_to_logger( 58 | self, logger: Logger, configuration: LoggingConfiguration 59 | ) -> None: 60 | for handler in logger.handlers: 61 | logger.removeHandler(handler) 62 | handler = StreamHandler(configuration.output_file) 63 | handler.setFormatter(Formatter("%(levelname)s: %(message)s")) 64 | logger.addHandler(handler) 65 | logger.setLevel(configuration.log_level) 66 | -------------------------------------------------------------------------------- /nix_prefetch_github/prefetch.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Optional 5 | 6 | from nix_prefetch_github.hash import is_sha1_hash 7 | from nix_prefetch_github.interfaces import ( 8 | GithubRepository, 9 | PrefetchedRepository, 10 | PrefetchFailure, 11 | PrefetchOptions, 12 | PrefetchResult, 13 | RevisionIndexFactory, 14 | UrlHasher, 15 | ) 16 | 17 | 18 | @dataclass(frozen=True) 19 | class PrefetcherImpl: 20 | url_hasher: UrlHasher 21 | revision_index_factory: RevisionIndexFactory 22 | 23 | def prefetch_github( 24 | self, 25 | repository: GithubRepository, 26 | rev: Optional[str], 27 | prefetch_options: PrefetchOptions, 28 | ) -> PrefetchResult: 29 | revision: Optional[str] 30 | if rev is not None and self._is_proper_revision_hash(rev): 31 | revision = rev 32 | else: 33 | revision = self._detect_revision(repository, rev) 34 | if revision is None: 35 | return PrefetchFailure( 36 | reason=PrefetchFailure.Reason.unable_to_locate_revision 37 | ) 38 | return self._prefetch_github(repository, revision, prefetch_options) 39 | 40 | def _prefetch_github( 41 | self, 42 | repository: GithubRepository, 43 | revision: str, 44 | prefetch_options: PrefetchOptions, 45 | ) -> PrefetchResult: 46 | prefetched_repo = self.url_hasher.calculate_hash_sum( 47 | repository=repository, 48 | revision=revision, 49 | prefetch_options=prefetch_options, 50 | ) 51 | if prefetched_repo is None: 52 | return PrefetchFailure( 53 | reason=PrefetchFailure.Reason.unable_to_calculate_hash_sum 54 | ) 55 | return PrefetchedRepository( 56 | repository=repository, 57 | hash_sum=prefetched_repo.hash_sum, 58 | rev=revision, 59 | options=prefetch_options, 60 | store_path=prefetched_repo.store_path, 61 | ) 62 | 63 | def _is_proper_revision_hash(self, revision: str) -> bool: 64 | return is_sha1_hash(revision) 65 | 66 | def _detect_revision( 67 | self, repository: GithubRepository, revision: Optional[str] 68 | ) -> Optional[str]: 69 | actual_rev: Optional[str] 70 | revision_index = self.revision_index_factory.get_revision_index(repository) 71 | if revision_index is None: 72 | return None 73 | if revision is None: 74 | actual_rev = revision_index.get_revision_by_name("HEAD") 75 | else: 76 | actual_rev = revision_index.get_revision_by_name(revision) 77 | return actual_rev 78 | -------------------------------------------------------------------------------- /nix_prefetch_github/presenter/__init__.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | from nix_prefetch_github.interfaces import ( 5 | CommandLineView, 6 | PrefetchFailure, 7 | PrefetchResult, 8 | RepositoryRenderer, 9 | ViewModel, 10 | ) 11 | from nix_prefetch_github.prefetch import PrefetchedRepository 12 | 13 | 14 | @dataclass 15 | class PresenterImpl: 16 | view: CommandLineView 17 | repository_renderer: RepositoryRenderer 18 | 19 | def present(self, prefetch_result: PrefetchResult) -> None: 20 | stdout_lines: List[str] = [] 21 | stderr_lines: List[str] = [] 22 | return_code: int = 0 23 | if isinstance(prefetch_result, PrefetchedRepository): 24 | stdout_lines.append( 25 | self.repository_renderer.render_prefetched_repository(prefetch_result) 26 | ) 27 | elif isinstance(prefetch_result, PrefetchFailure): 28 | stderr_lines.append(self.render_prefetch_failure(prefetch_result)) 29 | return_code = 1 30 | else: 31 | raise Exception(f"Renderer received unexpected value {prefetch_result}") 32 | model = ViewModel( 33 | exit_code=return_code, 34 | stderr_lines=stderr_lines, 35 | stdout_lines=stdout_lines, 36 | ) 37 | self.view.render_view_model(model) 38 | 39 | def render_prefetch_failure(self, failure: PrefetchFailure) -> str: 40 | message = ( 41 | f"Prefetch failed: {failure.reason}. {self._explain_error(failure.reason)}" 42 | ) 43 | return message 44 | 45 | def _explain_error(self, reason: PrefetchFailure.Reason) -> str: 46 | if reason == PrefetchFailure.Reason.unable_to_locate_revision: 47 | return "nix-prefetch-github failed to find a matching revision to download from github. Have you spelled the repository owner, repository name and revision name correctly?" 48 | else: 49 | return "nix-prefetch-github failed to calculate a sha256 hash for the requested github repository. Do you have nix-prefetch-git, nix-prefetch-url and nix-build in your PATH?" 50 | -------------------------------------------------------------------------------- /nix_prefetch_github/presenter/repository_renderer.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dataclasses import dataclass 3 | from typing import Any, Dict, Optional 4 | 5 | from nix_prefetch_github.interfaces import ( 6 | GithubAPI, 7 | PrefetchedRepository, 8 | RenderingFormat, 9 | RepositoryRenderer, 10 | ) 11 | from nix_prefetch_github.templates import output_template 12 | 13 | 14 | class NixRepositoryRenderer: 15 | def render_prefetched_repository(self, repository: PrefetchedRepository) -> str: 16 | return output_template( 17 | owner=repository.repository.owner, 18 | repo=repository.repository.name, 19 | rev=repository.rev, 20 | hash_sum=repository.hash_sum, 21 | fetch_submodules=repository.options.fetch_submodules, 22 | leave_dot_git=repository.options.leave_dot_git, 23 | deep_clone=repository.options.deep_clone, 24 | ) 25 | 26 | 27 | class JsonRepositoryRenderer: 28 | DEFAULTS = { 29 | "fetchSubmodules": False, 30 | "leaveDotGit": False, 31 | "deepClone": False, 32 | } 33 | 34 | def render_to_json(self, repository: PrefetchedRepository) -> Dict[str, Any]: 35 | output: Dict[str, Any] = { 36 | "owner": repository.repository.owner, 37 | "repo": repository.repository.name, 38 | "rev": repository.rev, 39 | "hash": repository.hash_sum, 40 | } 41 | if repository.options.deep_clone != self.DEFAULTS["deepClone"]: 42 | output["deepClone"] = repository.options.deep_clone 43 | if repository.options.fetch_submodules != self.DEFAULTS["fetchSubmodules"]: 44 | output["fetchSubmodules"] = repository.options.fetch_submodules 45 | if repository.options.leave_dot_git != self.DEFAULTS["leaveDotGit"]: 46 | output["leaveDotGit"] = repository.options.leave_dot_git 47 | return output 48 | 49 | def render_prefetched_repository(self, repository: PrefetchedRepository) -> str: 50 | return json.dumps( 51 | self.render_to_json(repository), 52 | indent=4, 53 | ) 54 | 55 | 56 | @dataclass 57 | class MetaRepositoryRenderer: 58 | json_renderer: JsonRepositoryRenderer 59 | github_api: GithubAPI 60 | 61 | def render_prefetched_repository(self, repository: PrefetchedRepository) -> str: 62 | src_output = self.json_renderer.render_to_json(repository) 63 | meta_output: Dict[str, Any] = dict() 64 | commit_timestamp = self.github_api.get_commit_date( 65 | repository.repository, repository.rev 66 | ) 67 | meta_output["storePath"] = repository.store_path 68 | if commit_timestamp: 69 | meta_output["commitDate"] = commit_timestamp.date().isoformat() 70 | meta_output["commitTimeOfDay"] = commit_timestamp.time().isoformat() 71 | return json.dumps( 72 | { 73 | "src": src_output, 74 | "meta": meta_output, 75 | }, 76 | indent=4, 77 | ) 78 | 79 | 80 | @dataclass 81 | class RenderingSelectorImpl: 82 | nix_renderer: RepositoryRenderer 83 | json_renderer: RepositoryRenderer 84 | meta_renderer: RepositoryRenderer 85 | selected_output_format: Optional[RenderingFormat] = None 86 | 87 | def set_rendering_format(self, rendering_format: RenderingFormat) -> None: 88 | self.selected_output_format = rendering_format 89 | 90 | def render_prefetched_repository(self, repository: PrefetchedRepository) -> str: 91 | if self.selected_output_format == RenderingFormat.nix: 92 | return self.nix_renderer.render_prefetched_repository(repository) 93 | elif self.selected_output_format == RenderingFormat.meta: 94 | return self.meta_renderer.render_prefetched_repository(repository) 95 | else: 96 | return self.json_renderer.render_prefetched_repository(repository) 97 | -------------------------------------------------------------------------------- /nix_prefetch_github/presenter/test_rendering_selector_impl.py: -------------------------------------------------------------------------------- 1 | from nix_prefetch_github.interfaces import ( 2 | GithubRepository, 3 | PrefetchedRepository, 4 | PrefetchOptions, 5 | RenderingFormat, 6 | ) 7 | from nix_prefetch_github.tests import BaseTestCase 8 | 9 | from .repository_renderer import RenderingSelectorImpl 10 | 11 | 12 | class RenderingSelectorImplTests(BaseTestCase): 13 | def setUp(self) -> None: 14 | super().setUp() 15 | self.nix_renderer = RepositoryRendererImpl("nix format") 16 | self.json_renderer = RepositoryRendererImpl("json format") 17 | self.meta_renderer = RepositoryRendererImpl("meta format") 18 | self.rendering_selector = RenderingSelectorImpl( 19 | nix_renderer=self.nix_renderer, 20 | json_renderer=self.json_renderer, 21 | meta_renderer=self.meta_renderer, 22 | ) 23 | 24 | def test_by_default_json_format_is_selectored(self) -> None: 25 | output = self.rendering_selector.render_prefetched_repository( 26 | self.get_repository() 27 | ) 28 | self.assertEqual(output, "json format") 29 | 30 | def test_that_output_format_is_nix_after_nix_output_was_selected(self) -> None: 31 | self.rendering_selector.set_rendering_format(RenderingFormat.nix) 32 | output = self.rendering_selector.render_prefetched_repository( 33 | self.get_repository() 34 | ) 35 | self.assertEqual(output, "nix format") 36 | 37 | def test_that_output_format_is_json_after_json_output_was_selected(self) -> None: 38 | self.rendering_selector.set_rendering_format(RenderingFormat.json) 39 | output = self.rendering_selector.render_prefetched_repository( 40 | self.get_repository() 41 | ) 42 | self.assertEqual(output, "json format") 43 | 44 | def test_that_output_of_json_renderer_is_respected(self) -> None: 45 | self.json_renderer.output = "json format 2" 46 | output = self.rendering_selector.render_prefetched_repository( 47 | self.get_repository() 48 | ) 49 | self.assertEqual(output, "json format 2") 50 | 51 | def test_that_output_of_nix_renderer_is_respected(self) -> None: 52 | self.rendering_selector.set_rendering_format(RenderingFormat.nix) 53 | self.nix_renderer.output = "nix format 2" 54 | output = self.rendering_selector.render_prefetched_repository( 55 | self.get_repository() 56 | ) 57 | self.assertEqual(output, "nix format 2") 58 | 59 | def test_that_meta_renderer_is_used_when_format_is_set_to_meta(self) -> None: 60 | self.rendering_selector.set_rendering_format(RenderingFormat.meta) 61 | output = self.rendering_selector.render_prefetched_repository( 62 | self.get_repository() 63 | ) 64 | self.assertEqual(output, "meta format") 65 | 66 | def get_repository(self) -> PrefetchedRepository: 67 | return PrefetchedRepository( 68 | repository=GithubRepository(owner="test", name="test"), 69 | hash_sum="", 70 | options=PrefetchOptions(), 71 | rev="", 72 | store_path="", 73 | ) 74 | 75 | 76 | class RepositoryRendererImpl: 77 | def __init__(self, output: str) -> None: 78 | self.output = output 79 | 80 | def render_prefetched_repository(self, repository: PrefetchedRepository) -> str: 81 | return self.output 82 | -------------------------------------------------------------------------------- /nix_prefetch_github/presenter/test_repository_renderer.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from unittest import TestCase 3 | 4 | from nix_prefetch_github.interfaces import ( 5 | GithubRepository, 6 | PrefetchedRepository, 7 | PrefetchOptions, 8 | RepositoryRenderer, 9 | ) 10 | from nix_prefetch_github.presenter.repository_renderer import ( 11 | JsonRepositoryRenderer, 12 | NixRepositoryRenderer, 13 | ) 14 | 15 | 16 | class GeneralRepositoryRendererTests(TestCase): 17 | def setUp(self) -> None: 18 | self.renderers: List[RepositoryRenderer] = [ 19 | NixRepositoryRenderer(), 20 | JsonRepositoryRenderer(), 21 | ] 22 | 23 | def test_that_rendering_prefetched_repo_with_and_without_dot_git_directory_produces_different_output( 24 | self, 25 | ) -> None: 26 | for renderer in self.renderers: 27 | with self.subTest(): 28 | without_dot_git = self._make_repository(leave_dot_git=False) 29 | with_dot_git = self._make_repository(leave_dot_git=True) 30 | self.assertNotEqual( 31 | renderer.render_prefetched_repository(without_dot_git), 32 | renderer.render_prefetched_repository(with_dot_git), 33 | ) 34 | 35 | def test_that_rendering_prefetched_repo_with_and_without_deep_clone_produces_different_output( 36 | self, 37 | ) -> None: 38 | renderers: List[RepositoryRenderer] = [ 39 | NixRepositoryRenderer(), 40 | JsonRepositoryRenderer(), 41 | ] 42 | for renderer in renderers: 43 | with self.subTest(): 44 | without_deep_clone = self._make_repository(deep_clone=False) 45 | with_deep_clone = self._make_repository(deep_clone=True) 46 | self.assertNotEqual( 47 | renderer.render_prefetched_repository(without_deep_clone), 48 | renderer.render_prefetched_repository(with_deep_clone), 49 | ) 50 | 51 | def _make_repository( 52 | self, 53 | leave_dot_git: bool = False, 54 | deep_clone: bool = False, 55 | store_path: str = "/test/store/path", 56 | ) -> PrefetchedRepository: 57 | return PrefetchedRepository( 58 | repository=GithubRepository(owner="test", name="test"), 59 | rev="test", 60 | hash_sum="test", 61 | options=PrefetchOptions(leave_dot_git=leave_dot_git, deep_clone=deep_clone), 62 | store_path=store_path, 63 | ) 64 | -------------------------------------------------------------------------------- /nix_prefetch_github/process_environment.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class ProcessEnvironmentImpl: 5 | def get_cwd(self) -> str: 6 | return os.getcwd() 7 | -------------------------------------------------------------------------------- /nix_prefetch_github/repository_detector.py: -------------------------------------------------------------------------------- 1 | import re 2 | from dataclasses import dataclass 3 | from logging import Logger 4 | from typing import Optional 5 | 6 | from nix_prefetch_github.interfaces import CommandRunner, GithubRepository 7 | 8 | 9 | @dataclass 10 | class RepositoryDetectorImpl: 11 | command_runner: CommandRunner 12 | logger: Logger 13 | 14 | def is_repository_dirty(self, directory: str) -> bool: 15 | returncode, _ = self.command_runner.run_command( 16 | command=["git", "diff", "HEAD", "--quiet"], 17 | cwd=directory, 18 | ) 19 | return returncode != 128 and returncode != 0 20 | 21 | def detect_github_repository( 22 | self, directory: str, remote_name: Optional[str] 23 | ) -> Optional[GithubRepository]: 24 | if remote_name is None: 25 | remote = "origin" 26 | else: 27 | remote = remote_name 28 | returncode, stdout = self.command_runner.run_command( 29 | command=["git", "remote", "get-url", remote], cwd=directory 30 | ) 31 | detected_url = detect_github_repository_from_remote_url(stdout) 32 | self.logger.info(f"Detected repository '{detected_url}' from '{remote}'") 33 | return detected_url 34 | 35 | def get_current_revision(self, directory: str) -> Optional[str]: 36 | exitcode, stdout = self.command_runner.run_command( 37 | command=["git", "rev-parse", "HEAD"], cwd=directory 38 | ) 39 | if exitcode != 0: 40 | return None 41 | return stdout[:-1] 42 | 43 | 44 | def detect_github_repository_from_remote_url(url: str) -> Optional[GithubRepository]: 45 | match = re.match( 46 | r"(git@github.com:|https://github.com/)(?P.+)/((?P.+)\.git|(?P.+))$", 47 | url, 48 | ) 49 | if not match: 50 | return None 51 | else: 52 | owner = match.group("owner") 53 | name = match.group("repo") or match.group("repo_with_prefix") 54 | return GithubRepository( 55 | name=name, 56 | owner=owner, 57 | ) 58 | -------------------------------------------------------------------------------- /nix_prefetch_github/revision_index.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | from nix_prefetch_github.list_remote import ListRemote 5 | 6 | 7 | @dataclass(frozen=True) 8 | class RevisionIndexImpl: 9 | remote_list: ListRemote 10 | 11 | def get_revision_by_name(self, name: str) -> Optional[str]: 12 | if self.remote_list is None: 13 | return None 14 | if (symref := self.remote_list.symref(name)) is not None: 15 | name = symref 16 | return ( 17 | self.remote_list.full_ref_name(name) 18 | or self.remote_list.branch(name) 19 | or self.remote_list.tag(f"{name}^{{}}") 20 | or self.remote_list.tag(name) 21 | ) 22 | -------------------------------------------------------------------------------- /nix_prefetch_github/revision_index_factory.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, Protocol 3 | 4 | from nix_prefetch_github.functor import map_or_none 5 | from nix_prefetch_github.interfaces import GithubRepository 6 | from nix_prefetch_github.list_remote import ListRemote 7 | from nix_prefetch_github.revision_index import RevisionIndexImpl 8 | 9 | 10 | class ListRemoteFactory(Protocol): 11 | def get_list_remote(self, repository: GithubRepository) -> Optional[ListRemote]: 12 | pass 13 | 14 | 15 | @dataclass(frozen=True) 16 | class RevisionIndexFactoryImpl: 17 | list_remote_factory: ListRemoteFactory 18 | 19 | def get_revision_index( 20 | self, repository: GithubRepository 21 | ) -> Optional[RevisionIndexImpl]: 22 | return map_or_none( 23 | RevisionIndexImpl, self.list_remote_factory.get_list_remote(repository) 24 | ) 25 | -------------------------------------------------------------------------------- /nix_prefetch_github/templates.py: -------------------------------------------------------------------------------- 1 | _OUTPUT_TEMPLATE = """let 2 | pkgs = import {{}}; 3 | in 4 | pkgs.fetchFromGitHub {{ 5 | owner = "{owner}"; 6 | repo = "{repo}"; 7 | rev = "{rev}"; 8 | hash = "{hash_sum}";{fetch_submodules}{leave_dot_git}{deep_clone} 9 | }} 10 | """ 11 | 12 | 13 | def _render_line_if_enabled(line: str, condition: bool) -> str: 14 | return f"\n {line};" if condition else "" 15 | 16 | 17 | def output_template( 18 | owner: str, 19 | repo: str, 20 | rev: str, 21 | hash_sum: str, 22 | fetch_submodules: bool, 23 | leave_dot_git: bool, 24 | deep_clone: bool, 25 | ) -> str: 26 | return _OUTPUT_TEMPLATE.format( 27 | owner=owner, 28 | repo=repo, 29 | rev=rev, 30 | hash_sum=hash_sum, 31 | fetch_submodules=_render_line_if_enabled( 32 | "fetchSubmodules = true", fetch_submodules 33 | ), 34 | leave_dot_git=_render_line_if_enabled("leaveDotGit = true", leave_dot_git), 35 | deep_clone=_render_line_if_enabled("deepClone = true", deep_clone), 36 | ) 37 | -------------------------------------------------------------------------------- /nix_prefetch_github/test_github.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime, timezone 3 | from unittest import TestCase 4 | 5 | from parameterized import parameterized 6 | 7 | from nix_prefetch_github.github import GithubAPIImpl 8 | from nix_prefetch_github.interfaces import GithubRepository 9 | from nix_prefetch_github.tests import network 10 | 11 | 12 | @network 13 | class GithubTests(TestCase): 14 | def setUp(self) -> None: 15 | self.logger = logging.getLogger() 16 | self.api = GithubAPIImpl(logger=self.logger, environment=dict()) 17 | 18 | def test_that_for_own_repo_latest_release_is_not_none(self) -> None: 19 | self.assertIsNotNone( 20 | self.api.get_tag_of_latest_release( 21 | GithubRepository( 22 | owner="seppeljordan", 23 | name="nix-prefetch-github", 24 | ) 25 | ) 26 | ) 27 | 28 | @parameterized.expand( 29 | [ 30 | ( 31 | "33285dcd1ee5850eccc0d620be7a03975b4ed2c0", 32 | datetime(2023, 12, 30, 14, 5, 55, tzinfo=timezone.utc), 33 | ), 34 | ( 35 | "c2da1e1a6fb379285a34ca6458f01f372e28a24e", 36 | datetime(2023, 7, 9, 10, 6, 1, tzinfo=timezone.utc), 37 | ), 38 | ] 39 | ) 40 | def test_that_for_own_repo_can_determin_the_commit_time_of_specific_commit( 41 | self, sha1: str, expected_datetime: datetime 42 | ) -> None: 43 | self.assertEqual( 44 | self.api.get_commit_date( 45 | GithubRepository( 46 | owner="seppeljordan", 47 | name="nix-prefetch-github", 48 | ), 49 | sha1, 50 | ), 51 | expected_datetime, 52 | ) 53 | -------------------------------------------------------------------------------- /nix_prefetch_github/test_hash.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from nix_prefetch_github.hash import SriHash 4 | 5 | 6 | class SriHashTests(TestCase): 7 | def test_that_hash_function_and_digest_can_be_extracted_from_valid_sri_hashes( 8 | self, 9 | ) -> None: 10 | examples = [ 11 | ( 12 | "sha384-H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO", 13 | "sha384", 14 | "H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO", 15 | ), 16 | ( 17 | "sha512-Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==", 18 | "sha512", 19 | "Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==", 20 | ), 21 | ( 22 | "sha384-H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO?fakeoption", 23 | "sha384", 24 | "H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO", 25 | ), 26 | ] 27 | for example, expected_algorithm, expected_digest in examples: 28 | with self.subTest(example): 29 | sri = SriHash.from_text(example) 30 | assert sri.hash_function == expected_algorithm 31 | assert sri.digest == expected_digest 32 | 33 | def test_that_options_are_extracted_from_valid_hashes(self) -> None: 34 | examples = [ 35 | ( 36 | "sha384-H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO", 37 | [], 38 | ), 39 | ( 40 | "sha384-H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO?fakeoption", 41 | ["fakeoption"], 42 | ), 43 | ] 44 | for example, expected_options in examples: 45 | with self.subTest(example): 46 | sri = SriHash.from_text(example) 47 | assert expected_options == sri.options 48 | 49 | def test_that_hash_with_options_can_be_displayed_correctly(self) -> None: 50 | examples = [ 51 | "sha384-H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO", 52 | "sha384-H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO?fakeoption", 53 | ] 54 | for example in examples: 55 | with self.subTest(example): 56 | sri = SriHash.from_text(example) 57 | assert str(sri) == example 58 | 59 | def test_that_with_invalid_format_raise_value_error(self) -> None: 60 | examples = [ 61 | "", 62 | "abc", 63 | ] 64 | for example in examples: 65 | with self.subTest(example): 66 | with self.assertRaises(ValueError): 67 | SriHash.from_text(example) 68 | -------------------------------------------------------------------------------- /nix_prefetch_github/test_hash_converter.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | from unittest import TestCase 3 | 4 | from nix_prefetch_github.command.command_runner import CommandRunnerImpl 5 | from nix_prefetch_github.hash_converter import HashConverterImpl 6 | from nix_prefetch_github.tests import requires_nix_build 7 | 8 | 9 | @requires_nix_build 10 | class HashConverterTests(TestCase): 11 | def setUp(self) -> None: 12 | self.hash_converter = HashConverterImpl( 13 | command_runner=CommandRunnerImpl(logger=getLogger(__name__)) 14 | ) 15 | 16 | def test_return_none_if_input_is_not_a_valid_hash(self) -> None: 17 | assert self.hash_converter.convert_sha256_to_sri("abc") is None 18 | 19 | def test_return_sri_hash_if_hash_is_valid_sha256_hash(self) -> None: 20 | assert ( 21 | self.hash_converter.convert_sha256_to_sri( 22 | "B5AlNwg6kbcaqUiQEC6jslCRKVpErXLMsKC+b9aPlrM=" 23 | ) 24 | == "sha256-B5AlNwg6kbcaqUiQEC6jslCRKVpErXLMsKC+b9aPlrM=" 25 | ) 26 | -------------------------------------------------------------------------------- /nix_prefetch_github/test_list_remote.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from nix_prefetch_github.list_remote import ListRemote 4 | 5 | 6 | class ListRemoteTests(TestCase): 7 | def setUp(self) -> None: 8 | self.output = """From git@github.com:seppeljordan/nix-prefetch-github.git 9 | ref: refs/heads/master HEAD 10 | 9ce3bcc3610ffeb36f53bc690682f48c8d311764 HEAD 11 | 9ce3bcc3610ffeb36f53bc690682f48c8d311764 refs/heads/master 12 | c4e967f4a80e0c030364884e92f2c3cc39ae3ef2 refs/heads/travis-setup 13 | 1234567789473873487438239389538913598723 refs/heads/test/branch 14 | c4e967f4a80e0c030364884e92f2c3cc39ae3ef2 refs/pull/1/head 15 | c4f752b05270fd1ab812c4f6a41ddcd8769eb2e6 refs/pull/2/head 16 | ac17b18f3ba68bcea84b563523dfe82729e49aa8 refs/pull/5/head 17 | f7e74db312def6d0e57028b2b630962c768eeb9f refs/pull/7/head 18 | b12ab7fe187924d8536d27b2ddf3bcccd2612b32 refs/tags/1.3 19 | cffdbcb3351f500b5ca8867a65261443b576b215 refs/tags/v2.0 20 | 0b63b78df5e5e17fa46cbdd8aac2b56e8622e5d3 refs/tags/v2.1 21 | 9ce3bcc3610ffeb36f53bc690682f48c8d311764 refs/tags/v2.2 22 | """ 23 | self.remote_list = ListRemote.from_git_ls_remote_output(self.output) 24 | 25 | def test_contains_master_branch(self) -> None: 26 | assert ( 27 | self.remote_list.branch("master") 28 | == "9ce3bcc3610ffeb36f53bc690682f48c8d311764" 29 | ) 30 | 31 | def test_branch_returns_none_for_unknown_branch(self) -> None: 32 | assert self.remote_list.branch("does not exist") is None 33 | 34 | def test_contains_HEAD_symref(self) -> None: 35 | assert self.remote_list.symref("HEAD") == "master" 36 | 37 | def test_symref_returns_none_for_unknown_reference_name(self) -> None: 38 | assert self.remote_list.symref("unknown") is None 39 | 40 | def test_contains_tag_v2_0(self) -> None: 41 | assert ( 42 | self.remote_list.tag("v2.0") == "cffdbcb3351f500b5ca8867a65261443b576b215" 43 | ) 44 | 45 | def test_tag_returns_none_for_unkown_tag(self) -> None: 46 | assert self.remote_list.tag("unkown") is None 47 | 48 | def test_branch_with_slash_is_recognized(self) -> None: 49 | assert ( 50 | self.remote_list.branch("test/branch") 51 | == "1234567789473873487438239389538913598723" 52 | ) 53 | 54 | def test_full_ref_name_resolves_refs_heads_master(self) -> None: 55 | assert ( 56 | self.remote_list.full_ref_name("refs/heads/master") 57 | == "9ce3bcc3610ffeb36f53bc690682f48c8d311764" 58 | ) 59 | 60 | def test_full_ref_name_returns_none_for_invalid_refs(self) -> None: 61 | assert self.remote_list.full_ref_name("blabla") is None 62 | -------------------------------------------------------------------------------- /nix_prefetch_github/test_list_remote_factory.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | from unittest import TestCase 3 | 4 | from nix_prefetch_github.command.command_runner import CommandRunnerImpl 5 | from nix_prefetch_github.interfaces import GithubRepository 6 | from nix_prefetch_github.list_remote_factory import ListRemoteFactoryImpl 7 | from nix_prefetch_github.tests import network 8 | 9 | 10 | @network 11 | class RemoteListFactoryTests(TestCase): 12 | def setUp(self) -> None: 13 | self.factory = ListRemoteFactoryImpl( 14 | command_runner=CommandRunnerImpl(getLogger(__name__)) 15 | ) 16 | 17 | def test_for_non_existing_repo_we_get_none(self) -> None: 18 | repository = GithubRepository( 19 | owner="seppeljordan", name="repo_does_not_exist_12653" 20 | ) 21 | remote_list = self.factory.get_list_remote(repository) 22 | self.assertIsNone(remote_list) 23 | 24 | def test_for_existing_repository_we_get_truthy_value(self) -> None: 25 | repository = GithubRepository(owner="seppeljordan", name="nix-prefetch-github") 26 | remote_list = self.factory.get_list_remote(repository) 27 | self.assertTrue(remote_list) 28 | 29 | def test_get_correct_reference_for_version_v2_3(self) -> None: 30 | repository = GithubRepository(owner="seppeljordan", name="nix-prefetch-github") 31 | remote_list = self.factory.get_list_remote(repository) 32 | assert remote_list 33 | self.assertEqual( 34 | remote_list.tag("v2.3"), "e632ce77435a4ab269c227c3ebcbaeaf746f8627" 35 | ) 36 | -------------------------------------------------------------------------------- /nix_prefetch_github/test_logging.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | from logging import ERROR, INFO, WARNING 3 | from unittest import TestCase 4 | 5 | from nix_prefetch_github.logging import LoggerFactoryImpl, LoggingConfiguration 6 | 7 | 8 | class LoggerFactoryTests(TestCase): 9 | def setUp(self) -> None: 10 | self.output_handle = StringIO() 11 | self.factory = LoggerFactoryImpl() 12 | 13 | def test_can_specify_file_to_log_to(self) -> None: 14 | self.factory.set_logging_configuration( 15 | LoggingConfiguration(output_file=self.output_handle) 16 | ) 17 | logger = self.factory.get_logger() 18 | logger.error("test output") 19 | self.assertLogged("test output") 20 | 21 | def test_by_default_log_warning_level(self) -> None: 22 | self.factory.set_logging_configuration( 23 | LoggingConfiguration(output_file=self.output_handle) 24 | ) 25 | logger = self.factory.get_logger() 26 | logger.warning("test output") 27 | self.assertLogged("test output") 28 | 29 | def test_can_specify_log_level_via_configuration(self) -> None: 30 | self.factory.set_logging_configuration( 31 | LoggingConfiguration(output_file=self.output_handle, log_level=ERROR) 32 | ) 33 | logger = self.factory.get_logger() 34 | logger.warning("test output") 35 | self.assertNotLogged("test output") 36 | 37 | def test_can_get_logger_without_specifying_configuration(self) -> None: 38 | self.factory.get_logger() 39 | 40 | def test_setting_logging_configuration_is_respected_for_newly_constructed_loggers( 41 | self, 42 | ) -> None: 43 | self.factory.set_logging_configuration( 44 | LoggingConfiguration( 45 | output_file=self.output_handle, 46 | log_level=WARNING, 47 | ) 48 | ) 49 | logger = self.factory.get_logger() 50 | logger.warning("test message") 51 | self.assertLogged("test message") 52 | 53 | def test_setting_logging_configuration_propagates_to_already_created_loggers( 54 | self, 55 | ) -> None: 56 | logger = self.factory.get_logger() 57 | self.factory.set_logging_configuration( 58 | LoggingConfiguration(output_file=self.output_handle, log_level=INFO) 59 | ) 60 | logger.info("test message") 61 | self.assertLogged("test message") 62 | 63 | def assertLogged(self, message: str) -> None: 64 | self.output_handle.seek(0) 65 | self.assertIn(message, self.output_handle.read()) 66 | 67 | def assertNotLogged(self, message: str) -> None: 68 | self.output_handle.seek(0) 69 | self.assertNotIn(message, self.output_handle.read()) 70 | -------------------------------------------------------------------------------- /nix_prefetch_github/test_prefetch.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Optional, cast 2 | from unittest import TestCase 3 | 4 | from nix_prefetch_github.interfaces import GithubRepository, PrefetchOptions 5 | from nix_prefetch_github.list_remote import ListRemote 6 | from nix_prefetch_github.prefetch import ( 7 | PrefetchedRepository, 8 | PrefetcherImpl, 9 | PrefetchFailure, 10 | PrefetchResult, 11 | ) 12 | from nix_prefetch_github.revision_index import RevisionIndexImpl 13 | from nix_prefetch_github.tests import FakeRevisionIndexFactory, FakeUrlHasher 14 | 15 | 16 | class PrefetcherTests(TestCase): 17 | def test_when_url_hasher_and_revision_index_fail_then_prefetching_also_fails( 18 | self, 19 | ) -> None: 20 | self.url_hasher.hash_sum = None 21 | self.revision_index_factory.revision_index = None 22 | result = self.prefetch_repository() 23 | self.assertFailure(result) 24 | 25 | def test_return_hash_that_was_returned_by_url_hasher(self) -> None: 26 | result = self.prefetch_repository() 27 | self.assertIsInstance(result, PrefetchedRepository) 28 | 29 | def test_return_expected_hash_from_url_hasher(self) -> None: 30 | result = self.prefetch_repository() 31 | self.assertSuccess(result, lambda r: r.hash_sum == self.expected_hash) 32 | 33 | def test_that_expected_store_path_is_returned(self) -> None: 34 | result = self.prefetch_repository() 35 | self.assertSuccess(result, lambda r: r.store_path == self.expected_store_path) 36 | 37 | def test_return_expected_revision_from_revision_index(self) -> None: 38 | result = self.prefetch_repository() 39 | self.assertSuccess(result, lambda r: r.rev == self.expected_revision) 40 | 41 | def test_cannot_prefetch_revision_that_does_not_exist(self) -> None: 42 | result = self.prefetch_repository(revision="does not exist") 43 | self.assertFailure( 44 | result, 45 | lambda f: f.reason == PrefetchFailure.Reason.unable_to_locate_revision, 46 | ) 47 | 48 | def test_fail_with_correct_reason_when_hash_could_not_be_calculated(self) -> None: 49 | self.url_hasher.hash_sum = None 50 | self.assertFailure( 51 | self.prefetch_repository(), 52 | lambda f: f.reason == PrefetchFailure.Reason.unable_to_calculate_hash_sum, 53 | ) 54 | 55 | def test_can_prefetch_revision_by_its_sha1_id(self) -> None: 56 | expected_revision = "4840fbf9ebd246d334c11335fc85747013230b05" 57 | self.assertSuccess( 58 | self.prefetch_repository(revision=expected_revision), 59 | lambda result: result.rev == expected_revision, 60 | ) 61 | 62 | def assertSuccess( 63 | self, 64 | result: PrefetchResult, 65 | prop: Callable[[PrefetchedRepository], bool] = lambda _: True, 66 | ) -> None: 67 | self.assertIsInstance(result, PrefetchedRepository) 68 | self.assertTrue(prop(cast(PrefetchedRepository, result))) 69 | 70 | def assertFailure( 71 | self, 72 | result: PrefetchResult, 73 | prop: Callable[[PrefetchFailure], bool] = lambda _: True, 74 | ) -> None: 75 | self.assertIsInstance(result, PrefetchFailure) 76 | self.assertTrue(prop(cast(PrefetchFailure, result))) 77 | 78 | def setUp(self) -> None: 79 | self.url_hasher = FakeUrlHasher() 80 | self.revision_index_factory = FakeRevisionIndexFactory() 81 | self.expected_hash = "test hash" 82 | self.expected_revision = "test ref" 83 | self.expected_store_path = "test path" 84 | self.url_hasher.hash_sum = self.expected_hash 85 | self.url_hasher.store_path = self.expected_store_path 86 | self.revision_index_factory.revision_index = RevisionIndexImpl( 87 | ListRemote( 88 | symrefs={"HEAD": "refs/heads/master"}, 89 | heads={"master": self.expected_revision}, 90 | ) 91 | ) 92 | self.prefetcher = PrefetcherImpl( 93 | self.url_hasher, 94 | self.revision_index_factory, 95 | ) 96 | self.repository = GithubRepository("test owner", "test name") 97 | 98 | def prefetch_repository( 99 | self, 100 | revision: Optional[str] = None, 101 | options: PrefetchOptions = PrefetchOptions(), 102 | ) -> PrefetchResult: 103 | return self.prefetcher.prefetch_github(self.repository, revision, options) 104 | -------------------------------------------------------------------------------- /nix_prefetch_github/test_prefetch_options.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from nix_prefetch_github.interfaces import PrefetchOptions 4 | 5 | 6 | class PrefetchOptionsTests(TestCase): 7 | def test_that_default_options_are_considered_safe(self) -> None: 8 | options = PrefetchOptions() 9 | self.assertTrue(options.is_safe()) 10 | 11 | def test_that_deep_clone_is_not_considered_safe(self) -> None: 12 | options = PrefetchOptions(deep_clone=True) 13 | self.assertFalse(options.is_safe()) 14 | 15 | def test_that_leave_dot_git_is_not_considered_safe(self) -> None: 16 | options = PrefetchOptions(leave_dot_git=True) 17 | self.assertFalse(options.is_safe()) 18 | -------------------------------------------------------------------------------- /nix_prefetch_github/test_presenter.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from unittest import TestCase 3 | 4 | from nix_prefetch_github.interfaces import ( 5 | GithubRepository, 6 | PrefetchedRepository, 7 | PrefetchFailure, 8 | PrefetchOptions, 9 | ViewModel, 10 | ) 11 | from nix_prefetch_github.presenter import PresenterImpl 12 | 13 | 14 | class PresenterTests(TestCase): 15 | def setUp(self) -> None: 16 | self.renderer = TestingRepositoryRenderer() 17 | self.view = FakeView() 18 | self.presenter = PresenterImpl( 19 | view=self.view, 20 | repository_renderer=self.renderer, 21 | ) 22 | self.repo = PrefetchedRepository( 23 | GithubRepository(owner="test", name="test"), 24 | rev="test", 25 | hash_sum="test", 26 | options=PrefetchOptions(), 27 | store_path="", 28 | ) 29 | 30 | def test_write_to_error_output_when_presenting_prefetch_failure(self) -> None: 31 | self.presenter.present( 32 | PrefetchFailure(reason=PrefetchFailure.Reason.unable_to_calculate_hash_sum) 33 | ) 34 | self.assertTrue(self.read_error_output()) 35 | 36 | def test_write_to_result_output_when_presenting_prefetched_repository(self) -> None: 37 | self.presenter.present(self.repo) 38 | self.assertTrue(self.read_result_output()) 39 | 40 | def test_nothing_is_written_to_result_output_when_presenting_prefetch_failure( 41 | self, 42 | ) -> None: 43 | self.presenter.present( 44 | PrefetchFailure(reason=PrefetchFailure.Reason.unable_to_calculate_hash_sum) 45 | ) 46 | self.assertFalse(self.read_result_output()) 47 | 48 | def test_nothing_is_written_to_error_output_when_presenting_prefetched_respository( 49 | self, 50 | ) -> None: 51 | self.presenter.present(self.repo) 52 | self.assertFalse(self.read_error_output()) 53 | 54 | def test_rendered_repository_is_writted_to_result_output(self) -> None: 55 | self.presenter.present(self.repo) 56 | self.assertIn( 57 | self.renderer.render_prefetched_repository(self.repo), 58 | self.read_result_output(), 59 | ) 60 | 61 | def test_that_exit_0_is_returned_when_repository_is_rendered(self) -> None: 62 | self.presenter.present(self.repo) 63 | self.assertExitCode(0) 64 | 65 | def test_that_exit_1_is_returned_when_failure_is_rendered(self) -> None: 66 | self.presenter.present( 67 | PrefetchFailure(reason=PrefetchFailure.Reason.unable_to_calculate_hash_sum) 68 | ) 69 | self.assertExitCode(1) 70 | 71 | def assertExitCode(self, code: int) -> None: 72 | self.assertEqual( 73 | self.view.exit_code, 74 | code, 75 | ) 76 | 77 | def read_error_output(self) -> str: 78 | return "\n".join(self.view.stderr) 79 | 80 | def read_result_output(self) -> str: 81 | return "\n".join(self.view.stdout) 82 | 83 | 84 | class TestingRepositoryRenderer: 85 | def render_prefetched_repository(self, repository: PrefetchedRepository) -> str: 86 | return str(repository) 87 | 88 | 89 | class FakeView: 90 | def __init__(self) -> None: 91 | self.stdout: List[str] = [] 92 | self.stderr: List[str] = [] 93 | self.exit_code: Optional[int] = None 94 | 95 | def render_view_model(self, model: ViewModel) -> None: 96 | for line in model.stderr_lines: 97 | self.stderr.append(line) 98 | for line in model.stdout_lines: 99 | self.stdout.append(line) 100 | self.exit_code = model.exit_code 101 | -------------------------------------------------------------------------------- /nix_prefetch_github/test_repository_detector.py: -------------------------------------------------------------------------------- 1 | import shlex 2 | import shutil 3 | import subprocess 4 | import tempfile 5 | from io import StringIO 6 | from logging import getLogger 7 | from unittest import TestCase 8 | 9 | from nix_prefetch_github.command.command_runner import CommandRunnerImpl 10 | from nix_prefetch_github.interfaces import GithubRepository 11 | from nix_prefetch_github.repository_detector import ( 12 | RepositoryDetectorImpl, 13 | detect_github_repository_from_remote_url, 14 | ) 15 | 16 | 17 | class GitTestCase(TestCase): 18 | def setUp(self) -> None: 19 | self.tmpdir = tempfile.mkdtemp() 20 | self.run_command("git init") 21 | self.run_command("git config user.name 'test user'") 22 | self.run_command("git config user.email test@email.test") 23 | self.stream = StringIO() 24 | self.logger = getLogger(__name__) 25 | self.detector = RepositoryDetectorImpl( 26 | command_runner=CommandRunnerImpl(logger=self.logger), logger=self.logger 27 | ) 28 | 29 | def tearDown(self) -> None: 30 | shutil.rmtree(self.tmpdir) 31 | 32 | def run_command(self, command: str) -> None: 33 | subprocess.run(shlex.split(command), cwd=self.tmpdir, capture_output=True) 34 | 35 | 36 | class IsRepositoryDirtyTests(GitTestCase): 37 | def test_empty_directories_arent_considered_dirty(self) -> None: 38 | is_dirty = self.detector.is_repository_dirty(directory=self.tmpdir) 39 | self.assertFalse(is_dirty) 40 | 41 | def test_check_git_repo_is_dirty_works_on_clean_repos(self) -> None: 42 | self.run_command("touch test.txt") 43 | self.run_command("git add test.txt") 44 | self.run_command("git commit -a -m 'test commit'") 45 | is_dirty = self.detector.is_repository_dirty(directory=self.tmpdir) 46 | self.assertFalse(is_dirty) 47 | 48 | def test_check_git_repo_is_dirty_works_on_dirty_repos(self) -> None: 49 | self.run_command("touch test.txt") 50 | self.run_command("git add test.txt") 51 | self.run_command("git commit -a -m 'test commit'") 52 | self.run_command("touch test2.txt") 53 | self.run_command("git add test2.txt") 54 | is_dirty = self.detector.is_repository_dirty(directory=self.tmpdir) 55 | self.assertTrue(is_dirty) 56 | 57 | 58 | class DetectGithubRepositoryTests(GitTestCase): 59 | def test_repository_without_remotes_returns_none(self) -> None: 60 | result = self.detector.detect_github_repository(self.tmpdir, remote_name=None) 61 | self.assertIsNone(result) 62 | 63 | def test_repository_with_github_remote_as_origin_returns_a_repository(self) -> None: 64 | self.run_command( 65 | "git remote add origin git@github.com:seppeljordan/nix-prefetch-github.git" 66 | ) 67 | result = self.detector.detect_github_repository(self.tmpdir, remote_name=None) 68 | self.assertIsInstance(result, GithubRepository) 69 | 70 | def test_repository_with_github_remote_with_remote_name_test_returns_a_repository( 71 | self, 72 | ) -> None: 73 | self.run_command( 74 | "git remote add test git@github.com:seppeljordan/nix-prefetch-github.git" 75 | ) 76 | result = self.detector.detect_github_repository(self.tmpdir, remote_name="test") 77 | self.assertIsInstance(result, GithubRepository) 78 | 79 | 80 | class DetectRevisionTests(GitTestCase): 81 | def test_for_repo_without_commit_return_none(self) -> None: 82 | self.assertIsNone(self.detector.get_current_revision(self.tmpdir)) 83 | 84 | def test_for_repo_with_commit_return_not_none(self) -> None: 85 | self.run_command("touch test.txt") 86 | self.run_command("git add test.txt") 87 | self.run_command("git commit -a -m 'test commit'") 88 | self.assertIsNotNone(self.detector.get_current_revision(self.tmpdir)) 89 | 90 | 91 | class DetectGithubRepositoryFromRemoteUrlTests(TestCase): 92 | def test_can_detect_valid_urls_and_handle_invalid_ones(self) -> None: 93 | fixture = [ 94 | ( 95 | "git@github.com:seppeljordan/nix-prefetch-github.git", 96 | GithubRepository( 97 | owner="seppeljordan", 98 | name="nix-prefetch-github", 99 | ), 100 | ), 101 | ( 102 | "https://github.com/seppeljordan/nix-prefetch-github.git", 103 | GithubRepository( 104 | owner="seppeljordan", 105 | name="nix-prefetch-github", 106 | ), 107 | ), 108 | ( 109 | "https://github.com/seppeljordan/nix-prefetch-github", 110 | GithubRepository( 111 | owner="seppeljordan", 112 | name="nix-prefetch-github", 113 | ), 114 | ), 115 | ( 116 | "invalid", 117 | None, 118 | ), 119 | ] 120 | for url, expected_repository in fixture: 121 | with self.subTest(): 122 | self.assertEqual( 123 | detect_github_repository_from_remote_url(url), 124 | expected_repository, 125 | ) 126 | -------------------------------------------------------------------------------- /nix_prefetch_github/test_revision_index.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from nix_prefetch_github.list_remote import ListRemote 4 | from nix_prefetch_github.revision_index import RevisionIndexImpl 5 | 6 | 7 | class RevisionIndexTests(TestCase): 8 | def setUp(self) -> None: 9 | self.index = RevisionIndexImpl(self.remote_list) 10 | 11 | @property 12 | def remote_list(self) -> ListRemote: 13 | return ListRemote( 14 | heads={"test-branch-1": "1", "test-branch-2": "2", "master": "master-ref"}, 15 | tags={ 16 | "tag-1^{}": "tag-1-ref-annotated", 17 | "tag-1": "tag-1-ref", 18 | "tag-2": "tag-2-ref", 19 | }, 20 | symrefs={"HEAD": "master"}, 21 | ) 22 | 23 | def test_revision_on_test_branch_1_is_1(self) -> None: 24 | expected_revision = "1" 25 | revision = self.index.get_revision_by_name("test-branch-1") 26 | self.assertEqual(revision, expected_revision) 27 | 28 | def test_revision_on_test_branch_2_is_2(self) -> None: 29 | expected_revision = "2" 30 | revision = self.index.get_revision_by_name("test-branch-2") 31 | self.assertEqual(revision, expected_revision) 32 | 33 | def test_revision_for_tag_1_is_tag_1_ref_annotated(self) -> None: 34 | revision = self.index.get_revision_by_name("tag-1") 35 | self.assertEqual(revision, "tag-1-ref-annotated") 36 | 37 | def test_revision_for_tag_2_is_tag_2_ref_annotated(self) -> None: 38 | revision = self.index.get_revision_by_name("tag-2") 39 | self.assertEqual(revision, "tag-2-ref") 40 | 41 | def test_head_ref_points_to_master_ref(self) -> None: 42 | revision = self.index.get_revision_by_name("HEAD") 43 | self.assertEqual(revision, "master-ref") 44 | 45 | def test_can_get_ref_for_refs_heads_master(self) -> None: 46 | revision = self.index.get_revision_by_name("refs/heads/master") 47 | self.assertEqual(revision, "master-ref") 48 | -------------------------------------------------------------------------------- /nix_prefetch_github/test_revision_index_factory.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from unittest import TestCase 3 | 4 | from nix_prefetch_github.interfaces import GithubRepository 5 | from nix_prefetch_github.list_remote import ListRemote 6 | from nix_prefetch_github.revision_index import RevisionIndexImpl 7 | from nix_prefetch_github.revision_index_factory import RevisionIndexFactoryImpl 8 | 9 | 10 | class FakeListRemoteFactory: 11 | def __init__(self) -> None: 12 | self.remote: Optional[ListRemote] = None 13 | 14 | def get_list_remote(self, repository: GithubRepository) -> Optional[ListRemote]: 15 | return self.remote 16 | 17 | 18 | class RevisionIndexFactoryTests(TestCase): 19 | def setUp(self) -> None: 20 | self.repository = GithubRepository(owner="test owner", name="test name") 21 | self.list_remote_factory = FakeListRemoteFactory() 22 | self.revision_index_factory = RevisionIndexFactoryImpl(self.list_remote_factory) 23 | 24 | def set_list_remote(self, list_remote: Optional[ListRemote]) -> None: 25 | self.list_remote_factory.remote = list_remote 26 | 27 | def test_list_remote_factory_returns_none_then_revision_index_factory_also_returns_none( 28 | self, 29 | ) -> None: 30 | self.set_list_remote(None) 31 | self.assertIsNone( 32 | self.revision_index_factory.get_revision_index(self.repository) 33 | ) 34 | 35 | def test_list_remote_facory_returns_a_remote_then_revision_index_factory_returns_a_revision( 36 | self, 37 | ) -> None: 38 | self.set_list_remote(ListRemote()) 39 | self.assertIsInstance( 40 | self.revision_index_factory.get_revision_index(self.repository), 41 | RevisionIndexImpl, 42 | ) 43 | -------------------------------------------------------------------------------- /nix_prefetch_github/tests.py: -------------------------------------------------------------------------------- 1 | from os import getenv 2 | from typing import Callable, Dict, List, Optional, Tuple 3 | from unittest import TestCase, skipIf 4 | 5 | from nix_prefetch_github.interfaces import ( 6 | CommandRunner, 7 | GithubRepository, 8 | PrefetchedRessource, 9 | PrefetchOptions, 10 | RenderingFormat, 11 | ) 12 | from nix_prefetch_github.logging import LoggingConfiguration 13 | from nix_prefetch_github.revision_index import RevisionIndexImpl 14 | 15 | _disabled_tests = set(filter(bool, getenv("DISABLED_TESTS", "").split(" "))) 16 | network = skipIf("network" in _disabled_tests, "networking tests are disabled") 17 | requires_nix_build = skipIf( 18 | "requires_nix_build" in _disabled_tests, "tests requiring nix build are disabled" 19 | ) 20 | 21 | 22 | class BaseTestCase(TestCase): 23 | pass 24 | 25 | 26 | class FakeUrlHasher: 27 | def __init__(self) -> None: 28 | self.hash_sum: Optional[str] = None 29 | self.store_path: Optional[str] = None 30 | 31 | def calculate_hash_sum( 32 | self, 33 | repository: GithubRepository, 34 | revision: str, 35 | prefetch_options: PrefetchOptions, 36 | ) -> Optional[PrefetchedRessource]: 37 | return ( 38 | PrefetchedRessource( 39 | hash_sum=self.hash_sum, 40 | store_path=self.store_path, 41 | ) 42 | if self.hash_sum and self.store_path 43 | else None 44 | ) 45 | 46 | 47 | class FakeRevisionIndexFactory: 48 | def __init__(self) -> None: 49 | self.revision_index: Optional[RevisionIndexImpl] = None 50 | 51 | def get_revision_index( 52 | self, repository: GithubRepository 53 | ) -> Optional[RevisionIndexImpl]: 54 | return self.revision_index 55 | 56 | 57 | class FakeLoggerManager: 58 | def __init__(self) -> None: 59 | self.configuration: Optional[LoggingConfiguration] = None 60 | 61 | def set_logging_configuration(self, configuration: LoggingConfiguration) -> None: 62 | self.configuration = configuration 63 | 64 | def assertLoggingConfiguration( 65 | self, 66 | condition: Optional[Callable[[LoggingConfiguration], bool]] = None, 67 | message: str = "", 68 | ) -> None: 69 | assert self.configuration 70 | if condition: 71 | assert condition(self.configuration), message 72 | 73 | 74 | class CommandRunnerTestImpl: 75 | def __init__(self, command_runner: CommandRunner): 76 | self.command_runner = command_runner 77 | self.commands_issued: List[List[str]] = list() 78 | 79 | def run_command( 80 | self, 81 | command: List[str], 82 | cwd: Optional[str] = None, 83 | environment_variables: Optional[Dict[str, str]] = None, 84 | merge_stderr: bool = False, 85 | ) -> Tuple[int, str]: 86 | self.commands_issued.append(list(command)) 87 | return self.command_runner.run_command( 88 | command, cwd, environment_variables, merge_stderr 89 | ) 90 | 91 | 92 | class RenderingFormatSelectorImpl: 93 | def __init__(self) -> None: 94 | self.selected_output_format: Optional[RenderingFormat] = None 95 | 96 | def set_rendering_format(self, rendering_format: RenderingFormat) -> None: 97 | self.selected_output_format = rendering_format 98 | -------------------------------------------------------------------------------- /nix_prefetch_github/url_hasher/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seppeljordan/nix-prefetch-github/8a4d3cac69a0fc8aaa6ec9f310067bdc82ace788/nix_prefetch_github/url_hasher/__init__.py -------------------------------------------------------------------------------- /nix_prefetch_github/url_hasher/nix_prefetch.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dataclasses import dataclass 3 | from logging import Logger 4 | from typing import List, Optional 5 | 6 | from nix_prefetch_github.interfaces import ( 7 | CommandRunner, 8 | GithubRepository, 9 | HashConverter, 10 | PrefetchedRessource, 11 | PrefetchOptions, 12 | ) 13 | 14 | 15 | @dataclass(frozen=True) 16 | class NixPrefetchUrlHasherImpl: 17 | command_runner: CommandRunner 18 | logger: Logger 19 | hash_converter: HashConverter 20 | 21 | def calculate_hash_sum( 22 | self, 23 | repository: GithubRepository, 24 | revision: str, 25 | prefetch_options: PrefetchOptions, 26 | ) -> Optional[PrefetchedRessource]: 27 | if self.is_default_prefetch_options(prefetch_options): 28 | return self.fetch_url(repository=repository, revision=revision) 29 | else: 30 | return self.fetch_git( 31 | repository=repository, 32 | revision=revision, 33 | prefetch_options=prefetch_options, 34 | ) 35 | 36 | def fetch_url( 37 | self, repository: GithubRepository, revision: str 38 | ) -> Optional[PrefetchedRessource]: 39 | repo_url = f"https://github.com/{repository.owner}/{repository.name}/archive/{revision}.tar.gz" 40 | _, output = self.command_runner.run_command( 41 | ["nix-prefetch-url", "--unpack", repo_url, "--print-path"], 42 | ) 43 | try: 44 | hash_sum, store_path = output.splitlines() 45 | except ValueError: 46 | return None 47 | sri_hash = self.calculate_sri_representation(hash_sum.strip()) 48 | if not sri_hash: 49 | return None 50 | else: 51 | return PrefetchedRessource( 52 | hash_sum=sri_hash, 53 | store_path=store_path, 54 | ) 55 | 56 | def fetch_git( 57 | self, 58 | repository: GithubRepository, 59 | revision: str, 60 | prefetch_options: PrefetchOptions, 61 | ) -> Optional[PrefetchedRessource]: 62 | repo_url = f"https://github.com/{repository.owner}/{repository.name}.git" 63 | command = ( 64 | ["nix-prefetch-git"] 65 | + self.prefetch_git_options(prefetch_options) 66 | + [repo_url, revision] 67 | ) 68 | _, output = self.command_runner.run_command(command) 69 | command_output_json = json.loads(output) 70 | sri_hash = self.calculate_sri_representation(command_output_json["sha256"]) 71 | if not sri_hash: 72 | return None 73 | else: 74 | return PrefetchedRessource( 75 | hash_sum=sri_hash, 76 | store_path=command_output_json["path"], 77 | ) 78 | 79 | def calculate_sri_representation(self, sha256: str) -> Optional[str]: 80 | return self.hash_converter.convert_sha256_to_sri(sha256) 81 | 82 | def is_default_prefetch_options(self, options: PrefetchOptions) -> bool: 83 | return options == PrefetchOptions() 84 | 85 | def prefetch_git_options(self, prefetch_options: PrefetchOptions) -> List[str]: 86 | options: List[str] = [] 87 | if prefetch_options.deep_clone: 88 | options.append("--deepClone") 89 | if prefetch_options.leave_dot_git or prefetch_options.deep_clone: 90 | options.append("--leave-dotGit") 91 | if prefetch_options.fetch_submodules: 92 | options.append("--fetch-submodules") 93 | return options 94 | -------------------------------------------------------------------------------- /nix_prefetch_github/url_hasher/test_nix_prefetch_url_hasher.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | from unittest import TestCase 3 | 4 | from parameterized import parameterized 5 | 6 | from nix_prefetch_github.command.command_runner import CommandRunnerImpl 7 | from nix_prefetch_github.hash_converter import HashConverterImpl 8 | from nix_prefetch_github.interfaces import GithubRepository, PrefetchOptions 9 | from nix_prefetch_github.tests import CommandRunnerTestImpl, network 10 | from nix_prefetch_github.url_hasher.nix_prefetch import NixPrefetchUrlHasherImpl 11 | 12 | 13 | @network 14 | class UrlHasherTests(TestCase): 15 | def setUp(self) -> None: 16 | self.command_runner = CommandRunnerTestImpl( 17 | command_runner=CommandRunnerImpl(getLogger(__name__)) 18 | ) 19 | hash_converter = HashConverterImpl(command_runner=self.command_runner) 20 | self.hasher = NixPrefetchUrlHasherImpl( 21 | command_runner=self.command_runner, 22 | logger=getLogger(), 23 | hash_converter=hash_converter, 24 | ) 25 | self.repository = GithubRepository( 26 | owner="git-up", 27 | name="test-repo-submodules", 28 | ) 29 | self.revision = "5a1dfa807759c39e3df891b6b46dfb2cf776c6ef" 30 | 31 | @parameterized.expand( 32 | [ 33 | ( 34 | "git-up", 35 | "test-repo-submodules", 36 | "5a1dfa807759c39e3df891b6b46dfb2cf776c6ef", 37 | PrefetchOptions(), 38 | "sha256-B5AlNwg6kbcaqUiQEC6jslCRKVpErXLMsKC+b9aPlrM=", 39 | "/nix/store/d9bp6cchg2scyjfqnpxh7ghmw6fjmxvf-5a1dfa807759c39e3df891b6b46dfb2cf776c6ef.tar.gz", 40 | ), 41 | ( 42 | "git-up", 43 | "test-repo-submodules", 44 | "5a1dfa807759c39e3df891b6b46dfb2cf776c6ef", 45 | PrefetchOptions(fetch_submodules=True), 46 | "sha256-wCo1YobyatxSOE85xQNSJw6jvufghFNHlZl4ToQjRHA=", 47 | "/nix/store/5zb52kqrzvyc2na8lprv8vnky5fjw8f3-test-repo-submodules-5a1dfa8", 48 | ), 49 | ( 50 | "git-up", 51 | "test-repo-submodules", 52 | "5a1dfa807759c39e3df891b6b46dfb2cf776c6ef", 53 | PrefetchOptions(leave_dot_git=True), 54 | "sha256-0Za18NiCiPL9KFG4OzgIsM11bXOeRofKoEHgScvlEQg=", 55 | "/nix/store/rx3yji4dpkqzqb60zxp9rz8ql8sxwd60-test-repo-submodules-5a1dfa8", 56 | ), 57 | ( 58 | "seppeljordan", 59 | "nix-prefetch-github", 60 | "9578399cadb1cb2b252438cf14663333e8c3ee00", 61 | PrefetchOptions(), 62 | "sha256-JFC1+y+FMs2TwWjJxlAKAyDbSLFBE9J65myp7+slp50=", 63 | "/nix/store/6wv3zc015amj7mc9krffskj447xyajf6-9578399cadb1cb2b252438cf14663333e8c3ee00.tar.gz", 64 | ), 65 | ( 66 | "seppeljordan", 67 | "nix-prefetch-github", 68 | "9578399cadb1cb2b252438cf14663333e8c3ee00", 69 | PrefetchOptions(fetch_submodules=True), 70 | "sha256-JFC1+y+FMs2TwWjJxlAKAyDbSLFBE9J65myp7+slp50=", 71 | "/nix/store/dsfrwkrjjlsnydb8blph0gxr7p0xbnlm-nix-prefetch-github-9578399", 72 | ), 73 | ] 74 | ) 75 | def test_well_known_configurations_for_their_expected_hashes_and_store_paths( 76 | self, 77 | owner: str, 78 | repo: str, 79 | revision: str, 80 | options: PrefetchOptions, 81 | expected_hash_sum: str, 82 | expected_store_path: str, 83 | ) -> None: 84 | prefetched_repo = self.hasher.calculate_hash_sum( 85 | repository=GithubRepository(owner=owner, name=repo), 86 | revision=revision, 87 | prefetch_options=options, 88 | ) 89 | assert prefetched_repo 90 | self.assertEqual(prefetched_repo.hash_sum, expected_hash_sum) 91 | self.assertEqual(prefetched_repo.store_path, expected_store_path) 92 | 93 | def test_that_experimental_feature_nix_command_is_enabled(self) -> None: 94 | self.hasher.calculate_hash_sum( 95 | repository=self.repository, 96 | revision=self.revision, 97 | prefetch_options=PrefetchOptions(), 98 | ) 99 | issued_nix_commands = list( 100 | filter(lambda c: c[0] == "nix", self.command_runner.commands_issued) 101 | ) 102 | self.assertTrue( 103 | all( 104 | command[1] == "--extra-experimental-features" 105 | and command[2] == "nix-command" 106 | for command in issued_nix_commands 107 | ), 108 | msg="Not all commands in %s do include '--extra-experimental-features nix-command'" 109 | % issued_nix_commands, 110 | ) 111 | 112 | def test_with_deep_clone(self) -> None: 113 | prefetch_options = PrefetchOptions(deep_clone=True) 114 | prefetched_repo = self.hasher.calculate_hash_sum( 115 | repository=self.repository, 116 | revision=self.revision, 117 | prefetch_options=prefetch_options, 118 | ) 119 | assert prefetched_repo 120 | -------------------------------------------------------------------------------- /nix_prefetch_github/use_cases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seppeljordan/nix-prefetch-github/8a4d3cac69a0fc8aaa6ec9f310067bdc82ace788/nix_prefetch_github/use_cases/__init__.py -------------------------------------------------------------------------------- /nix_prefetch_github/use_cases/prefetch_directory.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from logging import Logger 5 | from typing import Protocol 6 | 7 | from nix_prefetch_github.interfaces import ( 8 | Prefetcher, 9 | PrefetchOptions, 10 | Presenter, 11 | RepositoryDetector, 12 | ) 13 | 14 | 15 | class PrefetchDirectoryUseCase(Protocol): 16 | def prefetch_directory(self, request: Request) -> None: ... 17 | 18 | 19 | @dataclass 20 | class Request: 21 | prefetch_options: PrefetchOptions 22 | directory: str 23 | remote: str 24 | 25 | 26 | @dataclass 27 | class PrefetchDirectoryUseCaseImpl: 28 | presenter: Presenter 29 | prefetcher: Prefetcher 30 | repository_detector: RepositoryDetector 31 | logger: Logger 32 | 33 | def prefetch_directory(self, request: Request) -> None: 34 | if self.repository_detector.is_repository_dirty(request.directory): 35 | self.logger.warning(f"Git repository at `{request.directory}` is dirty") 36 | repository = self.repository_detector.detect_github_repository( 37 | request.directory, remote_name=request.remote 38 | ) 39 | revision = self.repository_detector.get_current_revision(request.directory) 40 | assert repository 41 | prefetch_result = self.prefetcher.prefetch_github( 42 | repository=repository, 43 | rev=revision, 44 | prefetch_options=request.prefetch_options, 45 | ) 46 | self.presenter.present(prefetch_result) 47 | -------------------------------------------------------------------------------- /nix_prefetch_github/use_cases/prefetch_github_repository.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Optional, Protocol 5 | 6 | from nix_prefetch_github.interfaces import ( 7 | Alerter, 8 | GithubRepository, 9 | Prefetcher, 10 | PrefetchOptions, 11 | Presenter, 12 | ) 13 | 14 | 15 | class PrefetchGithubRepositoryUseCase(Protocol): 16 | def prefetch_github_repository(self, request: Request) -> None: ... 17 | 18 | 19 | @dataclass 20 | class Request: 21 | repository: GithubRepository 22 | revision: Optional[str] 23 | prefetch_options: PrefetchOptions 24 | 25 | 26 | @dataclass 27 | class PrefetchGithubRepositoryUseCaseImpl: 28 | presenter: Presenter 29 | prefetcher: Prefetcher 30 | alerter: Alerter 31 | 32 | def prefetch_github_repository(self, request: Request) -> None: 33 | if not request.prefetch_options.is_safe(): 34 | self.alerter.alert_user_about_unsafe_prefetch_options( 35 | request.prefetch_options 36 | ) 37 | prefetch_result = self.prefetcher.prefetch_github( 38 | repository=request.repository, 39 | rev=request.revision, 40 | prefetch_options=request.prefetch_options, 41 | ) 42 | self.presenter.present(prefetch_result) 43 | -------------------------------------------------------------------------------- /nix_prefetch_github/use_cases/prefetch_latest_release.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Protocol 5 | 6 | from nix_prefetch_github.interfaces import ( 7 | GithubAPI, 8 | GithubRepository, 9 | Prefetcher, 10 | PrefetchOptions, 11 | Presenter, 12 | ) 13 | 14 | 15 | class PrefetchLatestReleaseUseCase(Protocol): 16 | def prefetch_latest_release(self, request: Request) -> None: ... 17 | 18 | 19 | @dataclass 20 | class Request: 21 | repository: GithubRepository 22 | prefetch_options: PrefetchOptions 23 | 24 | 25 | @dataclass 26 | class PrefetchLatestReleaseUseCaseImpl: 27 | presenter: Presenter 28 | prefetcher: Prefetcher 29 | github_api: GithubAPI 30 | 31 | def prefetch_latest_release(self, request: Request) -> None: 32 | revision = self.github_api.get_tag_of_latest_release(request.repository) 33 | prefetch_result = self.prefetcher.prefetch_github( 34 | repository=request.repository, 35 | rev=revision, 36 | prefetch_options=request.prefetch_options, 37 | ) 38 | self.presenter.present(prefetch_result) 39 | -------------------------------------------------------------------------------- /nix_prefetch_github/use_cases/test_prefetch_github_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, List, Optional, cast 2 | from unittest import TestCase 3 | 4 | from nix_prefetch_github.interfaces import ( 5 | GithubRepository, 6 | PrefetchedRepository, 7 | PrefetchOptions, 8 | PrefetchResult, 9 | ) 10 | from nix_prefetch_github.use_cases.prefetch_github_repository import ( 11 | PrefetchGithubRepositoryUseCaseImpl, 12 | Request, 13 | ) 14 | 15 | 16 | class UseCaseTests(TestCase): 17 | def setUp(self) -> None: 18 | self.prefetcher = FakePrefetcher() 19 | self.presenter = FakePresenter() 20 | self.alerter = FakeAlerter() 21 | self.use_case = PrefetchGithubRepositoryUseCaseImpl( 22 | presenter=self.presenter, 23 | prefetcher=self.prefetcher, 24 | alerter=self.alerter, 25 | ) 26 | 27 | def test_that_repository_is_prefetched_successfully_if_prefetcher_succeeds( 28 | self, 29 | ) -> None: 30 | request = self.make_request() 31 | self.use_case.prefetch_github_repository(request) 32 | self.assert_is_success() 33 | 34 | def test_that_user_is_alerted_if_leave_dot_git_option_is_requested(self) -> None: 35 | request = self.make_request( 36 | prefetch_options=PrefetchOptions(leave_dot_git=True) 37 | ) 38 | self.use_case.prefetch_github_repository(request) 39 | self.assert_alerted_user() 40 | 41 | def test_that_user_is_alerted_if_deep_clone_option_is_requested(self) -> None: 42 | request = self.make_request(prefetch_options=PrefetchOptions(deep_clone=True)) 43 | self.use_case.prefetch_github_repository(request) 44 | self.assert_alerted_user() 45 | 46 | def test_that_user_is_alerted_with_correct_prefetch_options(self) -> None: 47 | expected_options = PrefetchOptions(deep_clone=True) 48 | request = self.make_request(prefetch_options=expected_options) 49 | self.use_case.prefetch_github_repository(request) 50 | self.assert_alert_options( 51 | lambda options: options == expected_options, 52 | ) 53 | 54 | def test_that_user_is_not_alerted_if_leave_dot_git_and_deep_clone_are_false( 55 | self, 56 | ) -> None: 57 | request = self.make_request( 58 | prefetch_options=PrefetchOptions(leave_dot_git=False, deep_clone=False) 59 | ) 60 | self.use_case.prefetch_github_repository(request) 61 | self.assert_not_alerted_user() 62 | 63 | def make_request( 64 | self, 65 | prefetch_options: PrefetchOptions = PrefetchOptions(), 66 | ) -> Request: 67 | return Request( 68 | repository=GithubRepository(owner="owner", name="name"), 69 | revision=None, 70 | prefetch_options=prefetch_options, 71 | ) 72 | 73 | def assert_alerted_user(self) -> None: 74 | self.assertEqual( 75 | self.alerter.alert_count, 76 | 1, 77 | "Expected the user to be alerted once but they were not alerted", 78 | ) 79 | 80 | def assert_not_alerted_user(self) -> None: 81 | self.assertFalse( 82 | self.alerter.alert_count, 83 | f"Expected the user to NOT be alerted but they were alerted {self.alerter.alert_count} times.", 84 | ) 85 | 86 | def assert_alert_options( 87 | self, 88 | condition: Callable[[PrefetchOptions], bool], 89 | message: Optional[str] = None, 90 | ) -> None: 91 | self.assertIsNotNone(self.alerter.last_alert_options) 92 | self.assertTrue( 93 | condition(cast(PrefetchOptions, self.alerter.last_alert_options)), 94 | msg=message, 95 | ) 96 | 97 | def assert_is_success(self) -> None: 98 | results = self.presenter.results 99 | self.assertTrue(results) 100 | self.assertIsInstance( 101 | results[-1], 102 | PrefetchedRepository, 103 | ) 104 | 105 | 106 | class FakeAlerter: 107 | def __init__(self) -> None: 108 | self.alert_count: int = 0 109 | self.last_alert_options: Optional[PrefetchOptions] = None 110 | 111 | def alert_user_about_unsafe_prefetch_options( 112 | self, prefetch_options: PrefetchOptions 113 | ) -> None: 114 | self.alert_count += 1 115 | self.last_alert_options = prefetch_options 116 | 117 | 118 | class FakePrefetcher: 119 | def prefetch_github( 120 | self, 121 | repository: GithubRepository, 122 | rev: Optional[str], 123 | prefetch_options: PrefetchOptions, 124 | ) -> PrefetchResult: 125 | return PrefetchedRepository( 126 | repository=repository, 127 | rev="", 128 | hash_sum="", 129 | options=prefetch_options, 130 | store_path="", 131 | ) 132 | 133 | 134 | class FakePresenter: 135 | def __init__(self) -> None: 136 | self.results: List[PrefetchResult] = [] 137 | 138 | def present(self, prefetch_result: PrefetchResult) -> None: 139 | self.results.append(prefetch_result) 140 | -------------------------------------------------------------------------------- /nix_prefetch_github/version.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | HERE = os.path.dirname(__file__) 4 | with open(os.path.join(HERE, "VERSION")) as f: 5 | VERSION_STRING = f.read() 6 | -------------------------------------------------------------------------------- /nix_prefetch_github/views.py: -------------------------------------------------------------------------------- 1 | from sys import exit, stderr, stdout 2 | 3 | from nix_prefetch_github.presenter import ViewModel 4 | 5 | 6 | class CommandLineViewImpl: 7 | def render_view_model(self, model: ViewModel) -> None: 8 | for line in model.stderr_lines: 9 | print(line, file=stderr) 10 | for line in model.stdout_lines: 11 | print(line, file=stdout) 12 | exit(model.exit_code) 13 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | include = ''' 3 | (\.pyi?) 4 | | (^/test\-pypi\-install) 5 | ''' -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | VERSION=$(cat nix_prefetch_github/VERSION) 6 | 7 | python setup.py sdist 8 | twine upload "dist/nix-prefetch-github-${VERSION}.tar.gz" 9 | git tag "v${VERSION}" 10 | git push origin "v${VERSION}" 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = nix-prefetch-github 3 | author = Sebastian Jordan 4 | author_email = sebastian.jordan.mail@googlemail.com 5 | description = Prefetch source code from github for nix build tool 6 | long_description = file: docs/README.rst 7 | version = file: nix_prefetch_github/VERSION 8 | url = https://github.com/seppeljordan/nix-prefetch-github 9 | 10 | [options] 11 | python_requires = >= 3.11 12 | classifiers = 13 | Intended Audience :: Developers 14 | License :: OSI Approved :: GNU General Public License v3 (GPLv3) 15 | Topic :: Software Development :: Version Control :: Git 16 | Topic :: System :: Software Distribution 17 | Programming Language :: Python :: 3 :: Only 18 | Programming Language :: Python :: 3.11 19 | Programming Language :: Python :: 3.12 20 | package_dir = 21 | nix_prefetch_github = nix_prefetch_github 22 | packages = find: 23 | include_package_data = True 24 | 25 | [options.entry_points] 26 | console_scripts = 27 | nix-prefetch-github = nix_prefetch_github.__main__:main 28 | nix-prefetch-github-directory = nix_prefetch_github.cli.fetch_directory:main 29 | nix-prefetch-github-latest-release = nix_prefetch_github.cli.fetch_latest_release:main 30 | 31 | [mypy] 32 | check_untyped_defs = True 33 | disallow_untyped_defs = True 34 | files = .,test-pypi-install 35 | 36 | [mypy-setuptools,mypyc.build,parameterized] 37 | ignore_missing_imports = True 38 | 39 | [flake8] 40 | ignore = E501, W503, E704, E226 41 | exclude = 42 | .mypy_cache 43 | site-packages 44 | filename = 45 | *.py 46 | ./test-pypi-install 47 | 48 | [tool:isort] 49 | profile = black 50 | skip_glob = 51 | result/** 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /test-pypi-install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import annotations 3 | 4 | import argparse 5 | import os 6 | import shutil 7 | import subprocess 8 | from contextlib import contextmanager 9 | from dataclasses import dataclass 10 | from pathlib import Path 11 | from tempfile import TemporaryDirectory 12 | from typing import Generator, Optional 13 | 14 | 15 | class Main: 16 | def __init__(self, twine: Twine, version_getter: VersionGetter) -> None: 17 | self.twine = twine 18 | self.version = version_getter.get_version() 19 | self.package_name = "nix-prefetch-github" 20 | self.current_distribution = f"{self.package_name}-{self.version}.tar.gz" 21 | self.setup_path = "setup.py" 22 | self.distribution_directory = Path("dist") 23 | self.source_distribution_path = ( 24 | self.distribution_directory / self.current_distribution 25 | ).resolve() 26 | self._egg_info_path = Path("nix_prefetch_github.egg-info").resolve() 27 | 28 | def run_installation_test(self) -> None: 29 | with self._virtualenv() as virtualenv: 30 | self.install_source_distribution(virtualenv) 31 | self.run_binaries(virtualenv) 32 | 33 | def run_upload_test(self) -> None: 34 | with self._virtualenv() as virtualenv: 35 | # self.upload_source_distribution() 36 | self.install_from_index(virtualenv) 37 | self.run_binaries(virtualenv) 38 | 39 | def install_source_distribution(self, virtualenv: Virtualenv) -> None: 40 | self.build_source_distribution() 41 | virtualenv.install(str(self.source_distribution_path)) 42 | 43 | def run_binaries(self, virtualenv: Virtualenv) -> None: 44 | virtualenv.run("nix-prefetch-github --help") 45 | virtualenv.run("nix-prefetch-github-latest-release --help") 46 | virtualenv.run("nix-prefetch-github-directory --help") 47 | 48 | def upload_source_distribution(self) -> None: 49 | self.build_source_distribution() 50 | self.twine.upload(self.source_distribution_path) 51 | 52 | def install_from_index(self, virtualenv: Virtualenv) -> None: 53 | virtualenv.install(str(self.source_distribution_path)) 54 | virtualenv.install_without_deps( 55 | f"{self.package_name}=={self.version}", index="test-pypi" 56 | ) 57 | 58 | def build_source_distribution(self) -> None: 59 | try: 60 | shutil.rmtree(self._egg_info_path) 61 | except FileNotFoundError: 62 | pass 63 | subprocess.run( 64 | f"python {self.setup_path} sdist --dist-dir {self.distribution_directory}", 65 | shell=True, 66 | check=True, 67 | ) 68 | 69 | @contextmanager 70 | def _virtualenv(self) -> Generator[Virtualenv, None, None]: 71 | with TemporaryDirectory() as directory: 72 | yield Virtualenv(Path(directory) / "virtualenv") 73 | 74 | 75 | class VersionGetter: 76 | def get_version(self) -> str: 77 | with open("nix_prefetch_github/VERSION") as version_file: 78 | return version_file.read() 79 | 80 | 81 | class Virtualenv: 82 | def __init__(self, target_path: Path) -> None: 83 | self._target_path = target_path 84 | subprocess.run(["virtualenv", str(self._target_path)]) 85 | 86 | def run(self, command: str) -> None: 87 | env = dict(os.environ) 88 | try: 89 | del env["PYTHONPATH"] 90 | except KeyError: 91 | pass 92 | subprocess.run( 93 | str(self._target_path / "bin") + "/" + command, 94 | check=True, 95 | shell=True, 96 | env=env, 97 | cwd=self._target_path, 98 | ) 99 | 100 | def install(self, target: str) -> None: 101 | self.run(f"pip install {target}") 102 | 103 | def install_without_deps(self, target: str, index: Optional[str] = None) -> None: 104 | self.run(f"pip install --no-deps {target} {'-i '+index if index else ''}") 105 | 106 | 107 | class Twine: 108 | def upload(self, source_distribution: Path) -> None: 109 | subprocess.run( 110 | f"python -m twine upload -r test-pypi {source_distribution}", 111 | shell=True, 112 | check=True, 113 | ) 114 | 115 | 116 | class Configuration: 117 | def get(self) -> ApplicationConfiguration: 118 | arguments = self._parse_args() 119 | return ApplicationConfiguration(do_upload_test=arguments.upload_test) 120 | 121 | def _parse_args(self) -> argparse.Namespace: 122 | parser = argparse.ArgumentParser() 123 | parser.add_argument("--upload-test", action="store_true", dest="upload_test") 124 | parser.add_argument( 125 | "--no-upload-test", action="store_false", dest="upload_test" 126 | ) 127 | return parser.parse_args() 128 | 129 | 130 | @dataclass 131 | class ApplicationConfiguration: 132 | do_upload_test: bool 133 | 134 | 135 | if __name__ == "__main__": 136 | configuration = Configuration().get() 137 | twine = Twine() 138 | tester = Main(twine, VersionGetter()) 139 | tester.run_installation_test() 140 | if configuration.do_upload_test: 141 | tester.run_upload_test() 142 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seppeljordan/nix-prefetch-github/8a4d3cac69a0fc8aaa6ec9f310067bdc82ace788/tests/__init__.py -------------------------------------------------------------------------------- /tests/jraygauthier_nixos_secure_factory_git_ls_remote.txt: -------------------------------------------------------------------------------- 1 | 2f50490cfca970bb2efa06b64753b9b516853951 HEAD 2 | c74c1b7c8e9d2296c192bd4d9fa9e6f8386d80fe refs/heads/jrg/mass_rep_under_by_dash 3 | ad1a1d1d25870cc70cd7e708a73c874322064d96 refs/heads/jrg/mvp 4 | 54f73a340826d9f014fb0734b02954c254e1d7c2 refs/heads/jrg/prototype_gpg_and_gopass_sanboxes 5 | 01bd63c74e735e3827bc07f7c0290829200aad0b refs/heads/jrg/prototype_gpg_and_gopass_sanboxes_squashed 6 | ad1a1d1d25870cc70cd7e708a73c874322064d96 refs/heads/jrg/yaml_dot_fs_api_changes 7 | 2f50490cfca970bb2efa06b64753b9b516853951 refs/heads/master 8 | 09e14a87ede9328a83f24a150be77d402230f7e5 refs/pull/1/head 9 | -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | import shlex 5 | import shutil 6 | import subprocess 7 | import tempfile 8 | import unittest 9 | from os import path 10 | from pathlib import Path 11 | from typing import List, Optional 12 | 13 | from nix_prefetch_github.tests import network, requires_nix_build 14 | 15 | 16 | class TestCase(unittest.TestCase): 17 | def assertReturncode( 18 | self, command: List[str], expected_code: int, message: Optional[str] = None 19 | ) -> None: 20 | finished_process = subprocess.run( 21 | command, 22 | stdout=subprocess.PIPE, 23 | stderr=subprocess.PIPE, 24 | universal_newlines=True, 25 | ) 26 | self.assertEqual( 27 | finished_process.returncode, 28 | expected_code, 29 | msg=self._prepare_failed_command_message( 30 | command=command, 31 | expected_code=expected_code, 32 | process=finished_process, 33 | msg=message, 34 | ), 35 | ) 36 | 37 | def _prepare_failed_command_message( 38 | self, 39 | command: List[str], 40 | expected_code: int, 41 | process: subprocess.CompletedProcess, 42 | msg: Optional[str], 43 | ) -> str: 44 | result = f"Expected return code {expected_code} when running {command}, but got {process.returncode}" 45 | if msg is not None: 46 | result += ", " + msg 47 | result += f"\nstdout: {process.stdout}" 48 | result += f"\nstderr: {process.stderr}" 49 | return result 50 | 51 | 52 | @network 53 | @requires_nix_build 54 | class FlakeCheckTest(TestCase): 55 | def test_that_flake_check_runs_successfully(self) -> None: 56 | self.assertReturncode(["nix", "flake", "check", "--print-build-logs"], 0) 57 | 58 | 59 | @network 60 | @requires_nix_build 61 | class VersionFlagTests(TestCase): 62 | def setUp(self) -> None: 63 | self.directory = tempfile.mkdtemp() 64 | self.output = path.join(self.directory, "result") 65 | subprocess.run( 66 | ["nix", "build", "--out-link", self.output], capture_output=True, check=True 67 | ) 68 | 69 | def test_can_specify_version_flag(self) -> None: 70 | commands = [ 71 | "nix-prefetch-github", 72 | "nix-prefetch-github-directory", 73 | "nix-prefetch-github-latest-release", 74 | ] 75 | for command in commands: 76 | with self.subTest(msg=command): 77 | self.assertReturncode([f"{self.output}/bin/{command}", "--version"], 0) 78 | 79 | def tearDown(self) -> None: 80 | shutil.rmtree(self.directory) 81 | 82 | 83 | @network 84 | @requires_nix_build 85 | class NixEvaluationTests(TestCase): 86 | def setUp(self) -> None: 87 | self.directory = tempfile.mkdtemp() 88 | self.output = path.join(self.directory, "result") 89 | subprocess.run( 90 | ["nix", "build", "--out-link", self.output], capture_output=True, check=True 91 | ) 92 | 93 | def test_can_build_nix_expressions(self) -> None: 94 | expressions = [ 95 | [ 96 | f"{self.output}/bin/nix-prefetch-github", 97 | "seppeljordan", 98 | "nix-prefetch-github", 99 | "--nix", 100 | "-v", 101 | ], 102 | [ 103 | f"{self.output}/bin/nix-prefetch-github-latest-release", 104 | "seppeljordan", 105 | "nix-prefetch-github", 106 | "--nix", 107 | "-v", 108 | ], 109 | ] 110 | for expression in expressions: 111 | with self.subTest(msg=shlex.join(expression)): 112 | finished_process = subprocess.run( 113 | expression, capture_output=True, universal_newlines=True 114 | ) 115 | self.assertEqual(finished_process.returncode, 0) 116 | self.assertReturncode( 117 | ["nix-build", "-E", finished_process.stdout, "--no-out-link"], 0 118 | ) 119 | 120 | def tearDown(self) -> None: 121 | shutil.rmtree(self.directory) 122 | 123 | 124 | @network 125 | @requires_nix_build 126 | class JsonIntegrityTests(TestCase): 127 | def setUp(self) -> None: 128 | self.directory = tempfile.mkdtemp() 129 | self.output = path.join(self.directory, "result") 130 | subprocess.run( 131 | ["nix", "build", "--out-link", self.output], capture_output=True, check=True 132 | ) 133 | 134 | def tearDown(self) -> None: 135 | shutil.rmtree(self.directory) 136 | 137 | def test_can_load_json_output_as_json(self) -> None: 138 | expressions = [ 139 | [ 140 | f"{self.output}/bin/nix-prefetch-github", 141 | "seppeljordan", 142 | "nix-prefetch-github", 143 | "-v", 144 | ], 145 | [ 146 | f"{self.output}/bin/nix-prefetch-github-latest-release", 147 | "seppeljordan", 148 | "nix-prefetch-github", 149 | "-v", 150 | ], 151 | [ 152 | f"{self.output}/bin/nix-prefetch-github-directory", 153 | "-v", 154 | ], 155 | ] 156 | for expression in expressions: 157 | expression_shell_command = shlex.join(expression) 158 | with self.subTest(msg=expression_shell_command): 159 | finished_process = subprocess.run( 160 | expression, capture_output=True, universal_newlines=True 161 | ) 162 | self.assertEqual( 163 | finished_process.returncode, 164 | 0, 165 | msg="\n".join( 166 | [ 167 | f"Failed to execute {expression_shell_command}.", 168 | f"stdout: {finished_process.stdout}", 169 | f"stderr: {finished_process.stderr}", 170 | ] 171 | ), 172 | ) 173 | json.loads(finished_process.stdout) 174 | 175 | def test_can_use_json_output_as_input_for_fetch_from_github(self) -> None: 176 | expression = [ 177 | f"{self.output}/bin/nix-prefetch-github", 178 | "seppeljordan", 179 | "nix-prefetch-github", 180 | ] 181 | finished_process = subprocess.run( 182 | expression, capture_output=True, check=True, universal_newlines=True 183 | ) 184 | with open(Path(self.directory) / "output.json", "w") as handle: 185 | handle.write(finished_process.stdout) 186 | subprocess.run( 187 | [ 188 | "nix", 189 | "build", 190 | "--impure", 191 | "--expr", 192 | "with import {}; with builtins; fetchFromGitHub (fromJSON (readFile ./output.json))", 193 | ], 194 | check=True, 195 | cwd=self.directory, 196 | ) 197 | 198 | def test_can_use_json_output_as_input_when_meta_argument_is_specified( 199 | self, 200 | ) -> None: 201 | expression = [ 202 | f"{self.output}/bin/nix-prefetch-github", 203 | "seppeljordan", 204 | "nix-prefetch-github", 205 | "--meta", 206 | ] 207 | finished_process = subprocess.run( 208 | expression, capture_output=True, check=True, universal_newlines=True 209 | ) 210 | with open(Path(self.directory) / "output.json", "w") as handle: 211 | handle.write(finished_process.stdout) 212 | subprocess.run( 213 | [ 214 | "nix", 215 | "build", 216 | "--impure", 217 | "--expr", 218 | "with import {}; with builtins; fetchFromGitHub (fromJSON (readFile ./output.json)).src", 219 | ], 220 | check=True, 221 | cwd=self.directory, 222 | ) 223 | 224 | 225 | @network 226 | @requires_nix_build 227 | class StorePathOutputTests(TestCase): 228 | def setUp(self) -> None: 229 | self.directory = tempfile.mkdtemp() 230 | self.output = path.join(self.directory, "result") 231 | subprocess.run( 232 | ["nix", "build", "--out-link", self.output], capture_output=True, check=True 233 | ) 234 | 235 | def tearDown(self) -> None: 236 | shutil.rmtree(self.directory) 237 | 238 | def test_can_ls_store_path_from_output(self) -> None: 239 | process = subprocess.run( 240 | [ 241 | f"{self.output}/bin/nix-prefetch-github", 242 | "seppeljordan", 243 | "nix-prefetch-github", 244 | "--meta", 245 | ], 246 | capture_output=True, 247 | universal_newlines=True, 248 | ) 249 | output_json = json.loads(process.stdout) 250 | self.assertReturncode(["ls", output_json["meta"]["storePath"]], 0) 251 | 252 | 253 | if __name__ == "__main__": 254 | unittest.main() 255 | -------------------------------------------------------------------------------- /tests/test_issue_21.py: -------------------------------------------------------------------------------- 1 | """This module contains tests regarding the github issue #21. 2 | 3 | https://github.com/seppeljordan/nix-prefetch-github/issues/21 4 | """ 5 | 6 | import os 7 | from unittest import TestCase 8 | 9 | from nix_prefetch_github.interfaces import GithubRepository, PrefetchOptions 10 | from nix_prefetch_github.list_remote import ListRemote 11 | from nix_prefetch_github.prefetch import PrefetchedRepository, PrefetcherImpl 12 | from nix_prefetch_github.revision_index import RevisionIndexImpl 13 | from nix_prefetch_github.tests import FakeRevisionIndexFactory, FakeUrlHasher 14 | 15 | 16 | class TestIssue21(TestCase): 17 | @property 18 | def sensu_go_ls_remote_output(self) -> ListRemote: 19 | with open( 20 | os.path.join(os.path.dirname(__file__), "sensu_go_git_ls_remote.txt") 21 | ) as handle: 22 | return ListRemote.from_git_ls_remote_output(handle.read()) 23 | 24 | def setUp(self) -> None: 25 | self.url_hasher = FakeUrlHasher() 26 | self.repository = GithubRepository( 27 | owner="sensu", 28 | name="sensu-go", 29 | ) 30 | self.revision_index_factory = FakeRevisionIndexFactory() 31 | self.prefetcher = PrefetcherImpl(self.url_hasher, self.revision_index_factory) 32 | 33 | def test_prefetch_sensu_go_5_11(self) -> None: 34 | self.url_hasher.hash_sum = "TEST_HASH_SUM" 35 | self.url_hasher.store_path = "test/path" 36 | self.revision_index_factory.revision_index = RevisionIndexImpl( 37 | self.sensu_go_ls_remote_output 38 | ) 39 | result = self.prefetcher.prefetch_github( 40 | repository=self.repository, 41 | rev="5.11.0", 42 | prefetch_options=PrefetchOptions(), 43 | ) 44 | assert isinstance(result, PrefetchedRepository) 45 | assert result.rev == "dd8f160a9033ecb5ad0384baf6a9965fa7bd3c17" 46 | assert result.hash_sum == "TEST_HASH_SUM" 47 | -------------------------------------------------------------------------------- /tests/test_issue_22.py: -------------------------------------------------------------------------------- 1 | """This module contains tests regarding the github issue #22. 2 | 3 | https://github.com/seppeljordan/nix-prefetch-github/issues/22 4 | """ 5 | 6 | import os 7 | from unittest import TestCase 8 | 9 | from nix_prefetch_github.interfaces import GithubRepository, PrefetchOptions 10 | from nix_prefetch_github.list_remote import ListRemote 11 | from nix_prefetch_github.prefetch import PrefetchedRepository, PrefetcherImpl 12 | from nix_prefetch_github.revision_index import RevisionIndexImpl 13 | from nix_prefetch_github.tests import FakeRevisionIndexFactory, FakeUrlHasher 14 | 15 | 16 | class Issue22Tests(TestCase): 17 | @property 18 | def nixos_secure_factory_ls_remote_output(self) -> ListRemote: 19 | with open( 20 | os.path.join( 21 | os.path.dirname(__file__), 22 | "jraygauthier_nixos_secure_factory_git_ls_remote.txt", 23 | ) 24 | ) as handle: 25 | return ListRemote.from_git_ls_remote_output(handle.read()) 26 | 27 | def setUp(self) -> None: 28 | self.repository = GithubRepository( 29 | owner="jraygauthier", name="nixos-secure-factory" 30 | ) 31 | self.url_hasher = FakeUrlHasher() 32 | self.revision_index_factory = FakeRevisionIndexFactory() 33 | self.revision_index_factory.revision_index = RevisionIndexImpl( 34 | self.nixos_secure_factory_ls_remote_output 35 | ) 36 | self.prefetcher = PrefetcherImpl( 37 | self.url_hasher, 38 | self.revision_index_factory, 39 | ) 40 | 41 | def test_issue_22(self) -> None: 42 | self.url_hasher.hash_sum = "TEST_HASH_SUM" 43 | self.url_hasher.store_path = "test/path" 44 | result = self.prefetcher.prefetch_github( 45 | repository=self.repository, 46 | rev="jrg/mvp", 47 | prefetch_options=PrefetchOptions(), 48 | ) 49 | assert isinstance(result, PrefetchedRepository) 50 | assert result.rev == "ad1a1d1d25870cc70cd7e708a73c874322064d96" 51 | assert result.hash_sum == "TEST_HASH_SUM" 52 | -------------------------------------------------------------------------------- /update-readme: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | nix build 6 | emacs -Q --batch README.org -l elisp/update-readme.el 7 | pandoc README.org --to rst > docs/README.rst 8 | --------------------------------------------------------------------------------