├── .checkignore ├── .codeclimate.yaml ├── .coveragerc ├── .csslintrc ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── ---bug-report.md │ └── ---feature-request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── codeql.yml │ ├── lint.yml │ ├── logger.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pyup.yaml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── changes ├── .directory ├── 33.bugifx └── 88.feature ├── giturlparse ├── __init__.py ├── parser.py ├── platforms │ ├── __init__.py │ ├── assembla.py │ ├── base.py │ ├── bitbucket.py │ ├── friendcode.py │ ├── github.py │ └── gitlab.py ├── result.py └── tests │ ├── __init__.py │ ├── test_parse.py │ └── test_rewrite.py ├── pyproject.toml ├── requirements-test.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── tasks.py └── tox.ini /.checkignore: -------------------------------------------------------------------------------- 1 | giturlparse/tests/* 2 | -------------------------------------------------------------------------------- /.codeclimate.yaml: -------------------------------------------------------------------------------- 1 | languages: 2 | Ruby: false 3 | JavaScript: false 4 | PHP: false 5 | Python: true 6 | exclude_paths: 7 | - 'giturlparse/tests/*' 8 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = giturlparse 4 | 5 | [report] 6 | omit = 7 | # Regexes for lines to exclude from consideration 8 | exclude_lines = 9 | # Have to re-enable the standard pragma 10 | pragma: no cover 11 | 12 | # Don't complain about missing debug-only code: 13 | def __repr__ 14 | if self\.debug 15 | 16 | # Don't complain if tests don't hit defensive assertion code: 17 | raise AssertionError 18 | raise NotImplementedError 19 | 20 | # Don't complain if non-runnable code isn't run: 21 | if 0: 22 | if __name__ == .__main__.: 23 | 24 | ignore_errors = True 25 | 26 | [html] 27 | directory = coverage_html 28 | -------------------------------------------------------------------------------- /.csslintrc: -------------------------------------------------------------------------------- 1 | --exclude-exts=.min.css 2 | --ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | max_line_length = 120 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [*.rst] 17 | max_line_length = 120 18 | 19 | [*.py] 20 | max_line_length = 120 21 | 22 | [*.{scss,html}] 23 | indent_size = 2 24 | indent_style = space 25 | max_line_length = 120 26 | 27 | [*.{js,vue,json}] 28 | indent_size = 2 29 | max_line_length = 120 30 | 31 | [*.{yml,yaml}] 32 | indent_size = 2 33 | 34 | [Makefile] 35 | indent_style = tab 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'type: bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | ## Description 15 | 16 | 19 | 20 | ## Steps to reproduce 21 | 22 | 25 | 26 | ## Versions 27 | 28 | 31 | 32 | ## Expected behaviour 33 | 34 | 37 | 38 | ## Actual behaviour 39 | 40 | 43 | 44 | ## Additional information 45 | 46 | 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F389 Feature request" 3 | about: Share your idea, let's discuss it! 4 | title: '' 5 | labels: 'type: feature' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | ## Description 15 | 16 | 19 | 20 | ## Use cases 21 | 22 | 25 | 26 | ## Proposed solution 27 | 28 | 31 | 32 | ## Alternatives 33 | 34 | 37 | 38 | ## Additional information 39 | 40 | 43 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Describe: 4 | 5 | * Content of the pull request 6 | * Feature added / Problem fixed 7 | 8 | ## References 9 | 10 | Provide any github issue fixed (as in ``Fix #XYZ``) 11 | 12 | # Checklist 13 | 14 | * [ ] Code lint checked via `inv lint` 15 | * [ ] Tests added 16 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '37 3 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: 'ubuntu-latest' 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Use only 'java' to analyze code written in Java, Kotlin or both 38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v3 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v2 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v2 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@v2 75 | with: 76 | category: "/language:${{matrix.language}}" 77 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Code quality 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 9 | strategy: 10 | matrix: 11 | python-version: ["3.11.x"] 12 | toxenv: [ruff, isort, black, pypi-description, towncrier] 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | repository: ${{ github.event.pull_request.head.repo.full_name }} 17 | ref: ${{ github.event.pull_request.head.ref }} 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Cache pip 23 | uses: actions/cache@v3 24 | with: 25 | path: ~/.cache/pip 26 | key: ${{ runner.os }}-pip-${{ matrix.toxenv }} 27 | restore-keys: | 28 | ${{ runner.os }}-pip-${{ matrix.toxenv }} 29 | - name: Cache tox 30 | uses: actions/cache@v3 31 | with: 32 | path: .tox 33 | key: ${{ runner.os }}-lint-${{ matrix.toxenv }}-${{ hashFiles('setup.cfg') }} 34 | restore-keys: | 35 | ${{ runner.os }}-lint-${{ matrix.toxenv }}- 36 | - name: Install dependencies 37 | run: | 38 | python -m pip install --upgrade pip setuptools tox>4 39 | - name: Test with tox 40 | if: ${{ matrix.toxenv != 'towncrier' || (!contains(github.event.head_commit.message, '[pre-commit.ci]') && !contains(github.event.pull_request.body, 'pre-commit.ci start')) }} 41 | run: | 42 | tox -e${{ matrix.toxenv }} 43 | -------------------------------------------------------------------------------- /.github/workflows/logger.yml: -------------------------------------------------------------------------------- 1 | name: Event Logger 2 | on: push 3 | 4 | jobs: 5 | log-github-event-goodies: 6 | name: "LOG Everything on GitHub Event" 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Logging 10 | run: | 11 | echo '${{toJSON(github.event)}}' 12 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published,prereleased] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.x' 16 | - name: Cache pip 17 | uses: actions/cache@v3 18 | with: 19 | path: ~/.cache/pip 20 | key: ${{ runner.os }}-pip-${{ matrix.toxenv }} 21 | restore-keys: | 22 | ${{ runner.os }}-pip-${{ matrix.toxenv }} 23 | - name: Cache tox 24 | uses: actions/cache@v3 25 | with: 26 | path: .tox 27 | key: ${{ runner.os }}-tox-release-${{ hashFiles('setup.cfg') }} 28 | restore-keys: | 29 | ${{ runner.os }}-tox-release- 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip setuptools tox>4 33 | - name: Build and publish 34 | env: 35 | TWINE_USERNAME: __token__ 36 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 37 | run: | 38 | tox -erelease 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tox tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.11.x, 3.10.x, 3.9, 3.8] 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Cache pip 19 | uses: actions/cache@v3 20 | with: 21 | path: ~/.cache/pip 22 | key: ${{ runner.os }}-pip-${{ matrix.toxenv }} 23 | restore-keys: | 24 | ${{ runner.os }}-pip-${{ matrix.toxenv }} 25 | - name: Cache tox 26 | uses: actions/cache@v3 27 | with: 28 | path: .tox 29 | key: ${{ runner.os }}-tox-${{ format('py', matrix.python-version) }}-${{ hashFiles('setup.cfg') }} 30 | restore-keys: | 31 | ${{ runner.os }}-tox-${{ format('py', matrix.python-version) }}- 32 | - name: Install dependencies 33 | run: | 34 | sudo apt-get install gettext 35 | python -m pip install --upgrade pip tox>4 36 | - name: Test with tox 37 | env: 38 | TOX_ENV: ${{ format('py', matrix.python-version) }} 39 | COMMAND: coverage run 40 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | COVERALLS_SERVICE_NAME: github 42 | run: | 43 | tox -e$TOX_ENV 44 | .tox/$TOX_ENV/bin/coverage xml 45 | - name: Coveralls Parallel 46 | uses: coverallsapp/github-action@v2 47 | with: 48 | github-token: ${{ secrets.GITHUB_TOKEN }} 49 | parallel: true 50 | - uses: codecov/codecov-action@v3 51 | with: 52 | token: ${{ secrets.CODECOV_TOKEN }} 53 | flags: unittests 54 | file: ./coverage.xml 55 | fail_ci_if_error: false 56 | finish: 57 | needs: test 58 | if: ${{ always() }} 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Coveralls Finished 62 | uses: coverallsapp/github-action@v2 63 | with: 64 | parallel-finished: true 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | exclude: "(.idea|node_modules|.tox)" 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v5.0.0 7 | hooks: 8 | - id: trailing-whitespace 9 | exclude: "setup.cfg" 10 | - id: end-of-file-fixer 11 | - id: check-yaml 12 | - id: check-added-large-files 13 | - id: check-builtin-literals 14 | - id: check-executables-have-shebangs 15 | - id: check-merge-conflict 16 | - id: check-toml 17 | - id: fix-encoding-pragma 18 | args: 19 | - --remove 20 | - repo: https://github.com/PyCQA/isort 21 | rev: "6.0.1" 22 | hooks: 23 | - id: isort 24 | - repo: https://github.com/psf/black 25 | rev: 25.1.0 26 | hooks: 27 | - id: black 28 | - repo: https://github.com/astral-sh/ruff-pre-commit 29 | rev: 'v0.11.0' 30 | hooks: 31 | - id: ruff 32 | - repo: https://github.com/asottile/pyupgrade 33 | rev: v3.19.1 34 | hooks: 35 | - id: pyupgrade 36 | args: 37 | - --py3-plus 38 | - repo: local 39 | hooks: 40 | - id: towncrier 41 | name: towncrier 42 | entry: inv towncrier-check 43 | language: system 44 | pass_filenames: false 45 | always_run: true 46 | ci: 47 | skip: 48 | - towncrier 49 | -------------------------------------------------------------------------------- /.pyup.yaml: -------------------------------------------------------------------------------- 1 | update: all 2 | pin: False 3 | branch: 4 | schedule: "every day" 5 | search: True 6 | branch_prefix: pyup/ 7 | close_prs: True 8 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Author 6 | ------ 7 | 8 | * Aaron O'Mullan 9 | 10 | Fork maintainer 11 | ---------------- 12 | 13 | * Iacopo Spalletti 14 | 15 | Contributors 16 | ------------ 17 | 18 | * Alex Koshelev 19 | * Craig Tracey 20 | * Daniel Lenski 21 | * Dennis Schwertel 22 | * John Vandenberg 23 | * nooperpudd 24 | * Tony Osibov 25 | * Leonardo Cavallucci 26 | * Santo Cariotti 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | Please read the instructions `here `_ to start contributing to `giturlparse`. 9 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | ******* 4 | History 5 | ******* 6 | 7 | .. towncrier release notes start 8 | 9 | 0.12.0 (2023-09-24) 10 | =================== 11 | 12 | Features 13 | -------- 14 | 15 | - Add github/gitlab username:access_token parse support (#21) 16 | - Migrate to bump-my-version (#79) 17 | 18 | 19 | Bugfixes 20 | -------- 21 | 22 | - Fix Gitlab URLs with branch (#42) 23 | - Align tox.ini with github actions (#71) 24 | 25 | 26 | 0.11.1 (2023-08-04) 27 | =================== 28 | 29 | Bugfixes 30 | -------- 31 | 32 | - Remove debug print statements (#66) 33 | 34 | 35 | 0.11.0 (2023-08-03) 36 | =================== 37 | 38 | Features 39 | -------- 40 | 41 | - Add parsing variable for user to gitlab parser (#47) 42 | - Add support for Python 3.8+ (#48) 43 | 44 | 45 | Bugfixes 46 | -------- 47 | 48 | - Update tests invocation method to avoid future breakages (#29) 49 | - Update linting tools and fix code style (#34) 50 | - Add more github use cases (#43) 51 | - Fix parsing generic git url (#46) 52 | 53 | 54 | 0.10.0 (2020-12-05) 55 | =================== 56 | 57 | Features 58 | -------- 59 | 60 | - General matching improvements (#18) 61 | - Update tooling, drop python2 (#10213) 62 | 63 | 0.9.2 (2018-10-27) 64 | ================== 65 | 66 | * Removed "s" from the base platform regex 67 | * Fix license classifier in setup.py 68 | * Update meta files 69 | 70 | 0.9.1 (2018-01-20) 71 | ================== 72 | 73 | * First fork release 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | include requirements.txt 7 | recursive-include giturlparse *py 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | giturlparse 3 | =========== 4 | 5 | Parse & rewrite git urls (supports GitHub, Bitbucket, FriendCode, Assembla, Gitlab ...) 6 | 7 | This is a fork of giturlparse.py with updated parsers. 8 | 9 | Original project can be found at https://github.com/FriendCode/giturlparse.py 10 | 11 | ************ 12 | Installing 13 | ************ 14 | 15 | :: 16 | 17 | pip install giturlparse 18 | 19 | ****************** 20 | Examples 21 | ****************** 22 | 23 | Exposed attributes 24 | ================== 25 | 26 | * ``platform``: platform codename 27 | * ``host``: server hostname 28 | * ``resource``: same as ``host`` 29 | * ``port``: URL port (only if explicitly defined in URL) 30 | * ``protocol``: URL protocol (git, ssh, http/https) 31 | * ``protocols``: list of protocols explicitly defined in URL 32 | * ``user``: repository user 33 | * ``owner``: repository owner (user or organization) 34 | * ``repo``: repository name 35 | * ``name``: same as ``repo`` 36 | * ``groups``: list of groups - gitlab only 37 | * ``path``: path to file or directory (includes the branch name) - gitlab / github only 38 | * ``path_raw``: raw path starting from the repo name (might include platform keyword) - gitlab / github only 39 | * ``branch``: branch name (when parseable) - gitlab / github only 40 | * ``username``: username from ``:@`` gitlab / github urls 41 | * ``access_token``: access token from ``:@`` gitlab / github urls 42 | 43 | Parse 44 | ================== 45 | 46 | :: 47 | 48 | from giturlparse import parse 49 | 50 | p = parse('git@bitbucket.org:AaronO/some-repo.git') 51 | 52 | p.host, p.owner, p.repo 53 | 54 | # => ('bitbucket.org', 'AaronO', 'some-repo') 55 | 56 | 57 | Rewrite 58 | ================== 59 | 60 | :: 61 | 62 | from giturlparse import parse 63 | 64 | url = 'git@github.com:Org/Private-repo.git' 65 | 66 | p = parse(url) 67 | 68 | p.url2ssh, p.url2https, p.url2git, p.url2http 69 | # => ('git@github.com:Org/Private-repo.git', 'https://github.com/Org/Private-repo.git', 'git://github.com/Org/Private-repo.git', None) 70 | 71 | URLS 72 | ================== 73 | 74 | Alternative URLs for same repo:: 75 | 76 | from giturlparse import parse 77 | 78 | url = 'git@github.com:Org/Private-repo.git' 79 | 80 | parse(url).urls 81 | # => { 82 | # 'ssh': 'git@github.com:Org/Private-repo.git', 83 | # 'https': 'https://github.com/Org/Private-repo.git', 84 | # 'git': 'git://github.com/Org/Private-repo.git' 85 | # } 86 | 87 | Validate 88 | ================== 89 | 90 | :: 91 | 92 | from giturlparse import parse, validate 93 | 94 | url = 'git@github.com:Org/Private-repo.git' 95 | 96 | parse(url).valid 97 | # => True 98 | 99 | # Or 100 | 101 | validate(url) 102 | # => True 103 | 104 | Tests 105 | ================== 106 | 107 | :: 108 | 109 | python -munittest 110 | 111 | License 112 | ================== 113 | 114 | Apache v2 (Check out LICENSE file) 115 | -------------------------------------------------------------------------------- /changes/.directory: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nephila/giturlparse/4a11815131082943b9f79b968ab08cd429e7d94b/changes/.directory -------------------------------------------------------------------------------- /changes/33.bugifx: -------------------------------------------------------------------------------- 1 | Add support for more github urls styles 2 | -------------------------------------------------------------------------------- /changes/88.feature: -------------------------------------------------------------------------------- 1 | Switch to Coveralls Github action 2 | -------------------------------------------------------------------------------- /giturlparse/__init__.py: -------------------------------------------------------------------------------- 1 | from .parser import parse as _parse 2 | from .result import GitUrlParsed 3 | 4 | __author__ = "Iacopo Spalletti" 5 | __email__ = "i.spalletti@nephila.it" 6 | __version__ = "0.12.0" 7 | 8 | 9 | def parse(url, check_domain=True): 10 | return GitUrlParsed(_parse(url, check_domain)) 11 | 12 | 13 | def validate(url, check_domain=True): 14 | return parse(url, check_domain).valid 15 | -------------------------------------------------------------------------------- /giturlparse/parser.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | from .platforms import PLATFORMS 4 | 5 | SUPPORTED_ATTRIBUTES = ( 6 | "domain", 7 | "repo", 8 | "owner", 9 | "path_raw", 10 | "groups_path", 11 | "_user", 12 | "port", 13 | "url", 14 | "platform", 15 | "protocol", 16 | "username", 17 | "access_token", 18 | ) 19 | 20 | 21 | def parse(url, check_domain=True): 22 | # Values are None by default 23 | parsed_info = defaultdict(lambda: None) 24 | parsed_info["port"] = "" 25 | parsed_info["path_raw"] = "" 26 | parsed_info["groups_path"] = "" 27 | parsed_info["owner"] = "" 28 | 29 | # Defaults to all attributes 30 | map(parsed_info.setdefault, SUPPORTED_ATTRIBUTES) 31 | 32 | for name, platform in PLATFORMS: 33 | for protocol, regex in platform.COMPILED_PATTERNS.items(): 34 | # print(name, protocol, regex) 35 | # Match current regex against URL 36 | match = regex.match(url) 37 | 38 | # Skip if not matched 39 | if not match: 40 | continue 41 | 42 | # Skip if domain is bad 43 | domain = match.group("domain") 44 | # print('[%s] DOMAIN = %s' % (url, domain,)) 45 | if check_domain: 46 | if platform.DOMAINS and domain not in platform.DOMAINS: 47 | continue 48 | if platform.SKIP_DOMAINS and domain in platform.SKIP_DOMAINS: 49 | continue 50 | 51 | # add in platform defaults 52 | parsed_info.update(platform.DEFAULTS) 53 | 54 | # Get matches as dictionary 55 | matches = platform.clean_data(match.groupdict(default="")) 56 | 57 | # Update info with matches 58 | parsed_info.update(matches) 59 | 60 | # Update info with platform info 61 | parsed_info.update( 62 | { 63 | "url": url, 64 | "platform": name, 65 | "protocol": protocol, 66 | } 67 | ) 68 | return parsed_info 69 | 70 | # Empty if none matched 71 | return parsed_info 72 | -------------------------------------------------------------------------------- /giturlparse/platforms/__init__.py: -------------------------------------------------------------------------------- 1 | from .assembla import AssemblaPlatform 2 | from .base import BasePlatform 3 | from .bitbucket import BitbucketPlatform 4 | from .friendcode import FriendCodePlatform 5 | from .github import GitHubPlatform 6 | from .gitlab import GitLabPlatform 7 | 8 | # Supported platforms 9 | PLATFORMS = [ 10 | # name -> Platform object 11 | ("github", GitHubPlatform()), 12 | ("bitbucket", BitbucketPlatform()), 13 | ("friendcode", FriendCodePlatform()), 14 | ("assembla", AssemblaPlatform()), 15 | ("gitlab", GitLabPlatform()), 16 | # Match url 17 | ("base", BasePlatform()), 18 | ] 19 | -------------------------------------------------------------------------------- /giturlparse/platforms/assembla.py: -------------------------------------------------------------------------------- 1 | from .base import BasePlatform 2 | 3 | 4 | class AssemblaPlatform(BasePlatform): 5 | DOMAINS = ("git.assembla.com",) 6 | PATTERNS = { 7 | "ssh": r"(?P(git\+)?(?Pssh))?(://)?git@(?P.+?):(?P(?P.+)).git", 8 | "git": r"(?P(?Pgit))://(?P.+?)/(?P(?P.+)).git", 9 | } 10 | FORMATS = { 11 | "ssh": r"git@%(domain)s:%(repo)s%(dot_git)s", 12 | "git": r"git://%(domain)s/%(repo)s%(dot_git)s", 13 | } 14 | DEFAULTS = {"_user": "git"} 15 | -------------------------------------------------------------------------------- /giturlparse/platforms/base.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import re 3 | 4 | 5 | class BasePlatform: 6 | FORMATS = { 7 | "https": r"https://%(domain)s/%(repo)s%(dot_git)s", 8 | "ssh": r"git@%(domain)s:%(repo)s%(dot_git)s%(path_raw)s", 9 | "git": r"git://%(domain)s/%(repo)s%(dot_git)s%(path_raw)s", 10 | } 11 | 12 | PATTERNS = { 13 | "ssh": r"(?P<_user>.+)@(?P[^/]+?):(?P.+)(?:(\.git)?(/)?)", 14 | "http": r"(?P(?Phttp))://(?P[^/]+?)/(?P.+)(?:(\.git)?(/)?)", 15 | "https": r"(?P(?Phttps))://(?P[^/]+?)/(?P.+)(?:(\.git)?(/)?)", 16 | "git": r"(?P(?Pgit))://(?P[^/]+?)/(?P.+)(?:(\.git)?(/)?)", 17 | } 18 | 19 | # None means it matches all domains 20 | DOMAINS = None 21 | SKIP_DOMAINS = None 22 | DEFAULTS = {} 23 | 24 | def __init__(self): 25 | # Precompile PATTERNS 26 | self.COMPILED_PATTERNS = {proto: re.compile(regex, re.IGNORECASE) for proto, regex in self.PATTERNS.items()} 27 | 28 | # Supported protocols 29 | self.PROTOCOLS = self.PATTERNS.keys() 30 | 31 | if self.__class__ == BasePlatform: 32 | sub = [subclass.SKIP_DOMAINS for subclass in self.__class__.__subclasses__() if subclass.SKIP_DOMAINS] 33 | if sub: 34 | self.SKIP_DOMAINS = list(itertools.chain.from_iterable(sub)) 35 | 36 | @staticmethod 37 | def clean_data(data): 38 | data["path"] = "" 39 | data["branch"] = "" 40 | data["protocols"] = list(filter(lambda x: x, data.get("protocols", "").split("+"))) 41 | data["pathname"] = data.get("pathname", "").strip(":").rstrip("/") 42 | return data 43 | -------------------------------------------------------------------------------- /giturlparse/platforms/bitbucket.py: -------------------------------------------------------------------------------- 1 | from .base import BasePlatform 2 | 3 | 4 | class BitbucketPlatform(BasePlatform): 5 | PATTERNS = { 6 | "https": ( 7 | r"(?P(git\+)?(?Phttps))://(?P<_user>.+)@(?P.+?)" 8 | r"(?P/(?P.+)/(?P.+?)(?:\.git)?)$" 9 | ), 10 | "ssh": ( 11 | r"(?P(git\+)?(?Pssh))?(://)?git@(?P.+?):" 12 | r"(?P(?P.+)/(?P.+?)(?:\.git)?)$" 13 | ), 14 | } 15 | FORMATS = { 16 | "https": r"https://%(owner)s@%(domain)s/%(owner)s/%(repo)s%(dot_git)s", 17 | "ssh": r"git@%(domain)s:%(owner)s/%(repo)s%(dot_git)s", 18 | } 19 | DOMAINS = ("bitbucket.org",) 20 | DEFAULTS = {"_user": "git"} 21 | -------------------------------------------------------------------------------- /giturlparse/platforms/friendcode.py: -------------------------------------------------------------------------------- 1 | from .base import BasePlatform 2 | 3 | 4 | class FriendCodePlatform(BasePlatform): 5 | DOMAINS = ("friendco.de",) 6 | PATTERNS = { 7 | "https": ( 8 | r"(?P(git\+)?(?Phttps))://(?P.+?)/" 9 | r"(?P(?P.+)@user/(?P.+)).git" 10 | ), 11 | } 12 | FORMATS = { 13 | "https": r"https://%(domain)s/%(owner)s@user/%(repo)s%(dot_git)s", 14 | } 15 | -------------------------------------------------------------------------------- /giturlparse/platforms/github.py: -------------------------------------------------------------------------------- 1 | from .base import BasePlatform 2 | 3 | 4 | class GitHubPlatform(BasePlatform): 5 | PATTERNS = { 6 | "https": ( 7 | r"(?P(git\+)?(?Phttps))://" 8 | r"((?P[^/]+?):(?P[^/]+?)@)?(?P[^/]+?)" 9 | r"(?P/(?P[^/]+?)/(?P[^/]+?)(?:(\.git)?(/)?)(?P(/blob/|/tree/).+)?)$" 10 | ), 11 | "ssh": ( 12 | r"(?P(git\+)?(?Pssh))?(://)?git@(?P.+?)(?P(:|/)" 13 | r"(?P[^/]+)/(?P[^/]+?)(?:(\.git)?(/)?)" 14 | r"(?P(/blob/|/tree/).+)?)$" 15 | ), 16 | "git": ( 17 | r"(?P(?Pgit))://(?P.+?)" 18 | r"(?P/(?P[^/]+)/(?P[^/]+?)(?:(\.git)?(/)?)" 19 | r"(?P(/blob/|/tree/).+)?)$" 20 | ), 21 | } 22 | FORMATS = { 23 | "https": r"https://%(domain)s/%(owner)s/%(repo)s%(dot_git)s%(path_raw)s", 24 | "ssh": r"git@%(domain)s:%(owner)s/%(repo)s%(dot_git)s%(path_raw)s", 25 | "git": r"git://%(domain)s/%(owner)s/%(repo)s%(dot_git)s%(path_raw)s", 26 | } 27 | DOMAINS = ( 28 | "github.com", 29 | "gist.github.com", 30 | ) 31 | DEFAULTS = {"_user": "git"} 32 | 33 | @staticmethod 34 | def clean_data(data): 35 | data = BasePlatform.clean_data(data) 36 | if data["path_raw"].startswith("/blob/"): 37 | data["path"] = data["path_raw"].replace("/blob/", "") 38 | if data["path_raw"].startswith("/tree/"): 39 | data["branch"] = data["path_raw"].replace("/tree/", "") 40 | return data 41 | -------------------------------------------------------------------------------- /giturlparse/platforms/gitlab.py: -------------------------------------------------------------------------------- 1 | from .base import BasePlatform 2 | 3 | 4 | class GitLabPlatform(BasePlatform): 5 | PATTERNS = { 6 | "https": ( 7 | r"(?P(git\+)?(?Phttps))://" 8 | r"((?P[^/]+?):(?P[^/]+?)@)?(?P[^:/]+)(?P:[0-9]+)?" 9 | r"(?P/(?P[^/]+?)/" 10 | r"(?P.*?)?(?(groups_path)/)?(?P[^/]+?)(?:(\.git)?(/)?)" 11 | r"(?P(/blob/|/-/blob/|/-/tree/).+)?)$" 12 | ), 13 | "ssh": ( 14 | r"(?P(git\+)?(?Pssh))?(://)?(?P<_user>.+?)@(?P[^:/]+)(:)?(?P[0-9]+)?(?(port))?" 15 | r"(?P/?(?P[^/]+)/" 16 | r"(?P.*?)?(?(groups_path)/)?(?P[^/]+?)(?:(\.git)?(/)?)" 17 | r"(?P(/blob/|/-/blob/|/-/tree/).+)?)$" 18 | ), 19 | "git": ( 20 | r"(?P(?Pgit))://(?P[^:/]+):?(?P[0-9]+)?(?(port))?" 21 | r"(?P/(?P[^/]+?)/" 22 | r"(?P.*?)?(?(groups_path)/)?(?P[^/]+?)(?:(\.git)?(/)?)" 23 | r"(?P(/blob/|/-/blob/|/-/tree/).+)?)$" 24 | ), 25 | } 26 | FORMATS = { 27 | "https": r"https://%(domain)s/%(owner)s/%(groups_slash)s%(repo)s%(dot_git)s%(path_raw)s", 28 | "ssh": r"git@%(domain)s:%(port_slash)s%(owner)s/%(groups_slash)s%(repo)s%(dot_git)s%(path_raw)s", 29 | "git": r"git://%(domain)s%(port)s/%(owner)s/%(groups_slash)s%(repo)s%(dot_git)s%(path_raw)s", 30 | } 31 | SKIP_DOMAINS = ( 32 | "github.com", 33 | "gist.github.com", 34 | ) 35 | DEFAULTS = {"_user": "git", "port": ""} 36 | 37 | @staticmethod 38 | def clean_data(data): 39 | data = BasePlatform.clean_data(data) 40 | if data["path_raw"].startswith("/blob/"): 41 | data["path"] = data["path_raw"].replace("/blob/", "") 42 | if data["path_raw"].startswith("/-/blob/"): 43 | data["path"] = data["path_raw"].replace("/-/blob/", "") 44 | if data["path_raw"].startswith("/-/tree/"): 45 | data["branch"] = data["path_raw"].replace("/-/tree/", "") 46 | return data 47 | -------------------------------------------------------------------------------- /giturlparse/result.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | 3 | from .platforms import PLATFORMS 4 | 5 | # Possible values to extract from a Git Url 6 | REQUIRED_ATTRIBUTES = ( 7 | "domain", 8 | "repo", 9 | ) 10 | 11 | 12 | class GitUrlParsed: 13 | platform = None 14 | 15 | def __init__(self, parsed_info): 16 | self._parsed = parsed_info 17 | 18 | # Set parsed objects as attributes 19 | for k, v in parsed_info.items(): 20 | setattr(self, k, v) 21 | 22 | for name, platform in PLATFORMS: 23 | if name == self.platform: 24 | self._platform_obj = platform 25 | break 26 | 27 | def _valid_attrs(self): 28 | return all([getattr(self, attr, None) for attr in REQUIRED_ATTRIBUTES]) # NOQA 29 | 30 | @property 31 | def valid(self): 32 | return all( 33 | [ 34 | self._valid_attrs(), 35 | ] 36 | ) 37 | 38 | ## 39 | # Alias properties 40 | ## 41 | @property 42 | def host(self): 43 | return self.domain 44 | 45 | @property 46 | def resource(self): 47 | return self.domain 48 | 49 | @property 50 | def name(self): 51 | return self.repo 52 | 53 | @property 54 | def user(self): 55 | if hasattr(self, "_user"): 56 | return self._user 57 | 58 | return self.owner 59 | 60 | @property 61 | def groups(self): 62 | if self.groups_path: 63 | return self.groups_path.split("/") 64 | else: 65 | return [] 66 | 67 | def format(self, protocol): # noqa : A0003 68 | """Reformat URL to protocol.""" 69 | items = copy(self._parsed) 70 | items["port_slash"] = "%s/" % self.port if self.port else "" 71 | items["groups_slash"] = "%s/" % self.groups_path if self.groups_path else "" 72 | items["dot_git"] = "" if items["repo"].endswith(".git") else ".git" 73 | return self._platform_obj.FORMATS[protocol] % items 74 | 75 | @property 76 | def normalized(self): 77 | """Normalize URL.""" 78 | return self.format(self.protocol) 79 | 80 | ## 81 | # Rewriting 82 | ## 83 | @property 84 | def url2ssh(self): 85 | return self.format("ssh") 86 | 87 | @property 88 | def url2http(self): 89 | return self.format("http") 90 | 91 | @property 92 | def url2https(self): 93 | return self.format("https") 94 | 95 | @property 96 | def url2git(self): 97 | return self.format("git") 98 | 99 | # All supported Urls for a repo 100 | @property 101 | def urls(self): 102 | return {protocol: self.format(protocol) for protocol in self._platform_obj.PROTOCOLS} 103 | 104 | ## 105 | # Platforms 106 | ## 107 | @property 108 | def github(self): 109 | return self.platform == "github" 110 | 111 | @property 112 | def bitbucket(self): 113 | return self.platform == "bitbucket" 114 | 115 | @property 116 | def friendcode(self): 117 | return self.platform == "friendcode" 118 | 119 | @property 120 | def assembla(self): 121 | return self.platform == "assembla" 122 | 123 | @property 124 | def gitlab(self): 125 | return self.platform == "gitlab" 126 | 127 | ## 128 | # Get data as dict 129 | ## 130 | @property 131 | def data(self): 132 | return dict(self._parsed) 133 | -------------------------------------------------------------------------------- /giturlparse/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nephila/giturlparse/4a11815131082943b9f79b968ab08cd429e7d94b/giturlparse/tests/__init__.py -------------------------------------------------------------------------------- /giturlparse/tests/test_parse.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from giturlparse import parse 4 | 5 | # Test data 6 | VALID_PARSE_URLS = ( 7 | # Valid SSH, HTTPS, GIT 8 | ( 9 | "SSH", 10 | ( 11 | "git@github.com:Org/Repo.git", 12 | { 13 | "host": "github.com", 14 | "resource": "github.com", 15 | "user": "git", 16 | "port": "", 17 | "owner": "Org", 18 | "repo": "Repo", 19 | "name": "Repo", 20 | "groups": [], 21 | "path": "", 22 | "path_raw": "", 23 | "pathname": "Org/Repo.git", 24 | "branch": "", 25 | "protocol": "ssh", 26 | "protocols": [], 27 | "github": True, 28 | "bitbucket": False, 29 | "assembla": False, 30 | }, 31 | ), 32 | ), 33 | ( 34 | "SSH", 35 | ( 36 | "git@github.com/nephila/giturlparse", 37 | { 38 | "host": "github.com", 39 | "resource": "github.com", 40 | "user": "git", 41 | "port": "", 42 | "owner": "nephila", 43 | "repo": "giturlparse", 44 | "name": "giturlparse", 45 | "groups": [], 46 | "path": "", 47 | "path_raw": "", 48 | "pathname": "/nephila/giturlparse", 49 | "branch": "", 50 | "protocol": "ssh", 51 | "protocols": [], 52 | "github": True, 53 | "bitbucket": False, 54 | "assembla": False, 55 | }, 56 | ), 57 | ), 58 | ( 59 | "SSH", 60 | ( 61 | "git@github.com/nephila/giturlparse/", 62 | { 63 | "host": "github.com", 64 | "resource": "github.com", 65 | "user": "git", 66 | "port": "", 67 | "owner": "nephila", 68 | "repo": "giturlparse", 69 | "name": "giturlparse", 70 | "groups": [], 71 | "path": "", 72 | "path_raw": "", 73 | "pathname": "/nephila/giturlparse", 74 | "branch": "", 75 | "protocol": "ssh", 76 | "protocols": [], 77 | "github": True, 78 | "bitbucket": False, 79 | "assembla": False, 80 | }, 81 | ), 82 | ), 83 | ( 84 | "SSH", 85 | ( 86 | "git@github.com/nephila/giturlparse.git/", 87 | { 88 | "host": "github.com", 89 | "resource": "github.com", 90 | "user": "git", 91 | "port": "", 92 | "owner": "nephila", 93 | "repo": "giturlparse", 94 | "name": "giturlparse", 95 | "groups": [], 96 | "path": "", 97 | "path_raw": "", 98 | "pathname": "/nephila/giturlparse.git", 99 | "branch": "", 100 | "protocol": "ssh", 101 | "protocols": [], 102 | "github": True, 103 | "bitbucket": False, 104 | "assembla": False, 105 | }, 106 | ), 107 | ), 108 | ( 109 | "HTTPS", 110 | ( 111 | "git+https://github.com/Org/Repo.git", 112 | { 113 | "host": "github.com", 114 | "resource": "github.com", 115 | "user": "git", 116 | "port": "", 117 | "owner": "Org", 118 | "repo": "Repo", 119 | "name": "Repo", 120 | "groups": [], 121 | "path": "", 122 | "path_raw": "", 123 | "pathname": "/Org/Repo.git", 124 | "branch": "", 125 | "protocol": "https", 126 | "protocols": ["git", "https"], 127 | "github": True, 128 | "bitbucket": False, 129 | "assembla": False, 130 | }, 131 | ), 132 | ), 133 | ( 134 | "HTTPS", 135 | ( 136 | "https://username:access_token@github.com/Org/Repo.git", 137 | { 138 | "host": "github.com", 139 | "resource": "github.com", 140 | "user": "git", 141 | "port": "", 142 | "owner": "Org", 143 | "repo": "Repo", 144 | "name": "Repo", 145 | "groups": [], 146 | "path": "", 147 | "path_raw": "", 148 | "pathname": "/Org/Repo.git", 149 | "branch": "", 150 | "protocol": "https", 151 | "protocols": ["https"], 152 | "username": "username", 153 | "access_token": "access_token", 154 | "github": True, 155 | "bitbucket": False, 156 | "assembla": False, 157 | }, 158 | ), 159 | ), 160 | ( 161 | "HTTPS", 162 | ( 163 | "https://github.com/foo-bar/xpwn", 164 | { 165 | "host": "github.com", 166 | "resource": "github.com", 167 | "user": "git", 168 | "port": "", 169 | "owner": "foo-bar", 170 | "repo": "xpwn", 171 | "name": "xpwn", 172 | "groups": [], 173 | "path": "", 174 | "path_raw": "", 175 | "pathname": "/foo-bar/xpwn", 176 | "branch": "", 177 | "protocol": "https", 178 | "protocols": ["https"], 179 | "github": True, 180 | }, 181 | ), 182 | ), 183 | ( 184 | "GIT", 185 | ( 186 | "git://github.com/Org/Repo.git", 187 | { 188 | "host": "github.com", 189 | "resource": "github.com", 190 | "user": "git", 191 | "port": "", 192 | "owner": "Org", 193 | "repo": "Repo", 194 | "name": "Repo", 195 | "groups": [], 196 | "path": "", 197 | "path_raw": "", 198 | "pathname": "/Org/Repo.git", 199 | "branch": "", 200 | "protocol": "git", 201 | "protocols": ["git"], 202 | "github": True, 203 | }, 204 | ), 205 | ), 206 | ( 207 | "GIT", 208 | ( 209 | "git://github.com/foo-bar/xpwn", 210 | { 211 | "host": "github.com", 212 | "resource": "github.com", 213 | "user": "git", 214 | "port": "", 215 | "owner": "foo-bar", 216 | "repo": "xpwn", 217 | "name": "xpwn", 218 | "groups": [], 219 | "path": "", 220 | "path_raw": "", 221 | "pathname": "/foo-bar/xpwn", 222 | "branch": "", 223 | "protocol": "git", 224 | "protocols": ["git"], 225 | "github": True, 226 | }, 227 | ), 228 | ), 229 | ( 230 | "SSH", 231 | ( 232 | "ssh://git@github.com/Org/Repo.git", 233 | { 234 | "host": "github.com", 235 | "resource": "github.com", 236 | "user": "git", 237 | "port": "", 238 | "owner": "Org", 239 | "repo": "Repo", 240 | "name": "Repo", 241 | "groups": [], 242 | "path": "", 243 | "path_raw": "", 244 | "pathname": "/Org/Repo.git", 245 | "branch": "", 246 | "protocol": "ssh", 247 | "protocols": ["ssh"], 248 | "github": True, 249 | }, 250 | ), 251 | ), 252 | # BitBucket 253 | ( 254 | "SSH", 255 | ( 256 | "git@bitbucket.org:Org/Repo.git", 257 | { 258 | "host": "bitbucket.org", 259 | "resource": "bitbucket.org", 260 | "user": "git", 261 | "port": "", 262 | "owner": "Org", 263 | "repo": "Repo", 264 | "name": "Repo", 265 | "groups": [], 266 | "path": "", 267 | "path_raw": "", 268 | "pathname": "Org/Repo.git", 269 | "branch": "", 270 | "protocol": "ssh", 271 | "protocols": [], 272 | "platform": "bitbucket", 273 | }, 274 | ), 275 | ), 276 | # Gitlab 277 | ( 278 | "SSH", 279 | ( 280 | "git@host.org:9999/Org/Repo.git", 281 | { 282 | "host": "host.org", 283 | "resource": "host.org", 284 | "user": "git", 285 | "port": "9999", 286 | "owner": "Org", 287 | "repo": "Repo", 288 | "name": "Repo", 289 | "groups": [], 290 | "path": "", 291 | "path_raw": "", 292 | "pathname": "/Org/Repo.git", 293 | "branch": "", 294 | "protocol": "ssh", 295 | "protocols": [], 296 | "platform": "gitlab", 297 | }, 298 | ), 299 | ), 300 | ( 301 | "SSH", 302 | ( 303 | "git@host.org:9999/Org-hyphen/Repo.git", 304 | { 305 | "host": "host.org", 306 | "resource": "host.org", 307 | "user": "git", 308 | "port": "9999", 309 | "owner": "Org-hyphen", 310 | "repo": "Repo", 311 | "name": "Repo", 312 | "groups": [], 313 | "path": "", 314 | "path_raw": "", 315 | "pathname": "/Org-hyphen/Repo.git", 316 | "branch": "", 317 | "protocol": "ssh", 318 | "protocols": [], 319 | "platform": "gitlab", 320 | }, 321 | ), 322 | ), 323 | ( 324 | "SSH", 325 | ( 326 | "git@host.org:Org/Repo.git", 327 | { 328 | "host": "host.org", 329 | "resource": "host.org", 330 | "user": "git", 331 | "port": "", 332 | "owner": "Org", 333 | "repo": "Repo", 334 | "name": "Repo", 335 | "groups": [], 336 | "path": "", 337 | "path_raw": "", 338 | "pathname": "Org/Repo.git", 339 | "branch": "", 340 | "protocol": "ssh", 341 | "protocols": [], 342 | "platform": "gitlab", 343 | }, 344 | ), 345 | ), 346 | ( 347 | "SSH", 348 | ( 349 | "ssh://git@host.org:9999/Org/Repo.git", 350 | { 351 | "host": "host.org", 352 | "resource": "host.org", 353 | "user": "git", 354 | "port": "9999", 355 | "owner": "Org", 356 | "repo": "Repo", 357 | "name": "Repo", 358 | "groups": [], 359 | "path": "", 360 | "path_raw": "", 361 | "pathname": "/Org/Repo.git", 362 | "branch": "", 363 | "protocol": "ssh", 364 | "protocols": ["ssh"], 365 | "platform": "gitlab", 366 | }, 367 | ), 368 | ), 369 | ( 370 | "HTTPS", 371 | ( 372 | "https://host.org/Org/Repo.git", 373 | { 374 | "host": "host.org", 375 | "resource": "host.org", 376 | "user": "git", 377 | "port": "", 378 | "owner": "Org", 379 | "repo": "Repo", 380 | "name": "Repo", 381 | "groups": [], 382 | "path": "", 383 | "path_raw": "", 384 | "pathname": "/Org/Repo.git", 385 | "branch": "", 386 | "protocol": "https", 387 | "protocols": ["https"], 388 | "platform": "gitlab", 389 | }, 390 | ), 391 | ), 392 | ( 393 | "HTTPS", 394 | ( 395 | "https://github.com/nephila/giturlparse/blob/master/giturlparse/github.py", 396 | { 397 | "host": "github.com", 398 | "resource": "github.com", 399 | "port": "", 400 | "user": "git", 401 | "owner": "nephila", 402 | "repo": "giturlparse", 403 | "name": "giturlparse", 404 | "groups": [], 405 | "path": "master/giturlparse/github.py", 406 | "path_raw": "/blob/master/giturlparse/github.py", 407 | "pathname": "/nephila/giturlparse/blob/master/giturlparse/github.py", 408 | "branch": "", 409 | "protocol": "https", 410 | "protocols": ["https"], 411 | "platform": "github", 412 | }, 413 | ), 414 | ), 415 | ( 416 | "HTTPS", 417 | ( 418 | "https://github.com/nephila/giturlparse/tree/feature/py37", 419 | { 420 | "host": "github.com", 421 | "resource": "github.com", 422 | "user": "git", 423 | "port": "", 424 | "owner": "nephila", 425 | "repo": "giturlparse", 426 | "name": "giturlparse", 427 | "groups": [], 428 | "path": "", 429 | "path_raw": "/tree/feature/py37", 430 | "pathname": "/nephila/giturlparse/tree/feature/py37", 431 | "branch": "feature/py37", 432 | "protocol": "https", 433 | "protocols": ["https"], 434 | "platform": "github", 435 | }, 436 | ), 437 | ), 438 | ( 439 | "HTTPS", 440 | ( 441 | "https://github.com/rubygems/rubygems/", 442 | { 443 | "host": "github.com", 444 | "resource": "github.com", 445 | "user": "git", 446 | "port": "", 447 | "owner": "rubygems", 448 | "repo": "rubygems", 449 | "name": "rubygems", 450 | "groups": [], 451 | "path": "", 452 | "path_raw": "", 453 | "pathname": "/rubygems/rubygems", 454 | "branch": "", 455 | "protocol": "https", 456 | "protocols": ["https"], 457 | "platform": "github", 458 | }, 459 | ), 460 | ), 461 | ( 462 | "HTTPS", 463 | ( 464 | "https://gitlab.com/nephila/giturlparse/blob/master/giturlparse/github.py", 465 | { 466 | "host": "gitlab.com", 467 | "resource": "gitlab.com", 468 | "user": "git", 469 | "port": "", 470 | "owner": "nephila", 471 | "repo": "giturlparse", 472 | "name": "giturlparse", 473 | "groups": [], 474 | "path": "master/giturlparse/github.py", 475 | "path_raw": "/blob/master/giturlparse/github.py", 476 | "pathname": "/nephila/giturlparse/blob/master/giturlparse/github.py", 477 | "branch": "", 478 | "protocol": "https", 479 | "protocols": ["https"], 480 | "platform": "gitlab", 481 | }, 482 | ), 483 | ), 484 | ( 485 | "HTTPS", 486 | ( 487 | "https://gitlab.com/nephila/git-urlparse/blob/master/giturlparse/github.py", 488 | { 489 | "host": "gitlab.com", 490 | "resource": "gitlab.com", 491 | "user": "git", 492 | "port": "", 493 | "owner": "nephila", 494 | "repo": "git-urlparse", 495 | "name": "git-urlparse", 496 | "groups": [], 497 | "path": "master/giturlparse/github.py", 498 | "path_raw": "/blob/master/giturlparse/github.py", 499 | "pathname": "/nephila/git-urlparse/blob/master/giturlparse/github.py", 500 | "branch": "", 501 | "protocol": "https", 502 | "protocols": ["https"], 503 | "platform": "gitlab", 504 | }, 505 | ), 506 | ), 507 | ( 508 | "HTTPS", 509 | ( 510 | "https://gitlab.com/nephila/git-urlparse/-/blob/master/giturlparse/github.py", 511 | { 512 | "host": "gitlab.com", 513 | "resource": "gitlab.com", 514 | "user": "git", 515 | "port": "", 516 | "owner": "nephila", 517 | "repo": "git-urlparse", 518 | "name": "git-urlparse", 519 | "groups": [], 520 | "path": "master/giturlparse/github.py", 521 | "path_raw": "/-/blob/master/giturlparse/github.py", 522 | "pathname": "/nephila/git-urlparse/-/blob/master/giturlparse/github.py", 523 | "branch": "", 524 | "protocol": "https", 525 | "protocols": ["https"], 526 | "platform": "gitlab", 527 | }, 528 | ), 529 | ), 530 | ( 531 | "HTTPS", 532 | ( 533 | "https://username:access_token@gitlab.com/nephila/giturlparse/-/blob/master/giturlparse/github.py", 534 | { 535 | "host": "gitlab.com", 536 | "resource": "gitlab.com", 537 | "user": "git", 538 | "port": "", 539 | "owner": "nephila", 540 | "repo": "giturlparse", 541 | "name": "giturlparse", 542 | "groups": [], 543 | "path": "master/giturlparse/github.py", 544 | "path_raw": "/-/blob/master/giturlparse/github.py", 545 | "pathname": "/nephila/giturlparse/-/blob/master/giturlparse/github.py", 546 | "branch": "", 547 | "username": "username", 548 | "access_token": "access_token", 549 | "protocol": "https", 550 | "protocols": ["https"], 551 | "platform": "gitlab", 552 | }, 553 | ), 554 | ), 555 | ( 556 | "HTTPS", 557 | ( 558 | "https://gitlab.com/nephila/group2/third-group/giturlparse/-/blob/master/" 559 | "giturlparse/platforms/github.py", 560 | { 561 | "host": "gitlab.com", 562 | "resource": "gitlab.com", 563 | "user": "git", 564 | "port": "", 565 | "owner": "nephila", 566 | "repo": "giturlparse", 567 | "name": "giturlparse", 568 | "groups": ["group2", "third-group"], 569 | "path": "master/giturlparse/platforms/github.py", 570 | "path_raw": "/-/blob/master/giturlparse/platforms/github.py", 571 | "pathname": "/nephila/group2/third-group/giturlparse/-/blob/master/" "giturlparse/platforms/github.py", 572 | "branch": "", 573 | "protocol": "https", 574 | "protocols": ["https"], 575 | "platform": "gitlab", 576 | }, 577 | ), 578 | ), 579 | ( 580 | "SSH", 581 | ( 582 | "git@host.org:9999/Org/Group/subGroup/Repo.git/-/blob/master/giturlparse/github.py", 583 | { 584 | "host": "host.org", 585 | "resource": "host.org", 586 | "user": "git", 587 | "port": "9999", 588 | "owner": "Org", 589 | "repo": "Repo", 590 | "name": "Repo", 591 | "groups": ["Group", "subGroup"], 592 | "path": "master/giturlparse/github.py", 593 | "path_raw": "/-/blob/master/giturlparse/github.py", 594 | "pathname": "/Org/Group/subGroup/Repo.git/-/blob/master/giturlparse/github.py", 595 | "branch": "", 596 | "protocol": "ssh", 597 | "protocols": [], 598 | "platform": "gitlab", 599 | }, 600 | ), 601 | ), 602 | ( 603 | "GIT", 604 | ( 605 | "git://host.org:9999/Org/Group/subGroup/Repo.git/-/blob/master/giturlparse/github.py", 606 | { 607 | "host": "host.org", 608 | "resource": "host.org", 609 | "user": "git", 610 | "port": "9999", 611 | "owner": "Org", 612 | "repo": "Repo", 613 | "name": "Repo", 614 | "groups": ["Group", "subGroup"], 615 | "path": "master/giturlparse/github.py", 616 | "path_raw": "/-/blob/master/giturlparse/github.py", 617 | "pathname": "/Org/Group/subGroup/Repo.git/-/blob/master/giturlparse/github.py", 618 | "branch": "", 619 | "protocol": "git", 620 | "protocols": ["git"], 621 | "platform": "gitlab", 622 | }, 623 | ), 624 | ), 625 | ( 626 | "GIT", 627 | ( 628 | "git://host.org/Org/Group/subGroup/Repo.git/-/blob/master/giturlparse/github.py", 629 | { 630 | "host": "host.org", 631 | "resource": "host.org", 632 | "user": "git", 633 | "port": "", 634 | "owner": "Org", 635 | "repo": "Repo", 636 | "name": "Repo", 637 | "groups": ["Group", "subGroup"], 638 | "path": "master/giturlparse/github.py", 639 | "path_raw": "/-/blob/master/giturlparse/github.py", 640 | "pathname": "/Org/Group/subGroup/Repo.git/-/blob/master/giturlparse/github.py", 641 | "branch": "", 642 | "protocol": "git", 643 | "protocols": ["git"], 644 | "platform": "gitlab", 645 | }, 646 | ), 647 | ), 648 | ( 649 | "GIT", 650 | ( 651 | "git://host.org:9999/Org/Group/subGroup/Repo.git/-/tree/feature/custom-branch", 652 | { 653 | "host": "host.org", 654 | "resource": "host.org", 655 | "user": "git", 656 | "port": "9999", 657 | "owner": "Org", 658 | "repo": "Repo", 659 | "name": "Repo", 660 | "groups": ["Group", "subGroup"], 661 | "path": "", 662 | "path_raw": "/-/tree/feature/custom-branch", 663 | "pathname": "/Org/Group/subGroup/Repo.git/-/tree/feature/custom-branch", 664 | "branch": "feature/custom-branch", 665 | "protocol": "git", 666 | "protocols": ["git"], 667 | "platform": "gitlab", 668 | }, 669 | ), 670 | ), 671 | ( 672 | "GIT", 673 | ( 674 | "git://git.buildroot.net/buildroot", 675 | { 676 | "host": "git.buildroot.net", 677 | "resource": "git.buildroot.net", 678 | "user": "", 679 | "port": "", 680 | "owner": "", 681 | "repo": "buildroot", 682 | "name": "buildroot", 683 | "groups": [], 684 | "path": "", 685 | "path_raw": "", 686 | "pathname": "", 687 | "branch": "", 688 | "protocol": "git", 689 | "protocols": ["git"], 690 | "github": False, 691 | }, 692 | ), 693 | ), 694 | ( 695 | "GIT", 696 | ( 697 | "joe@github.com-work:nephila/giturlparse.git", 698 | { 699 | "host": "github.com-work", 700 | "resource": "github.com-work", 701 | "user": "joe", 702 | "port": "", 703 | "owner": "nephila", 704 | "repo": "giturlparse", 705 | "name": "giturlparse", 706 | "groups": [], 707 | "path": "", 708 | "path_raw": "", 709 | "pathname": "nephila/giturlparse.git", 710 | "branch": "", 711 | "protocol": "ssh", 712 | "protocols": [], 713 | "github": False, 714 | "platform": "gitlab", 715 | }, 716 | ), 717 | ), 718 | ( 719 | "SSH", 720 | ( 721 | "git@gitlab.example.com/groupA/projectB.git", 722 | { 723 | "host": "gitlab.example.com", 724 | "resource": "gitlab.example.com", 725 | "user": "git", 726 | "port": "", 727 | "owner": "groupA", 728 | "repo": "projectB", 729 | "name": "projectB", 730 | "groups": [], 731 | "path": "", 732 | "path_raw": "", 733 | "pathname": "/groupA/projectB.git", 734 | "branch": "", 735 | "protocol": "ssh", 736 | "protocols": [], 737 | "github": False, 738 | "platform": "gitlab", 739 | }, 740 | ), 741 | ), 742 | ( 743 | "SSH", 744 | ( 745 | "ssh://git@gitlab.example.com/groupA/projectB.git", 746 | { 747 | "host": "gitlab.example.com", 748 | "resource": "gitlab.example.com", 749 | "user": "git", 750 | "port": "", 751 | "owner": "groupA", 752 | "repo": "projectB", 753 | "name": "projectB", 754 | "groups": [], 755 | "path": "", 756 | "path_raw": "", 757 | "pathname": "/groupA/projectB.git", 758 | "branch": "", 759 | "protocol": "ssh", 760 | "protocols": ["ssh"], 761 | "github": False, 762 | "platform": "gitlab", 763 | }, 764 | ), 765 | ), 766 | ) 767 | 768 | INVALID_PARSE_URLS = ( 769 | ("SSH No Username", "@github.com:Org/Repo.git"), 770 | ("SSH No Repo", "git@github.com:Org"), 771 | ("HTTPS No Repo", "https://github.com/Org"), 772 | ("GIT No Repo", "git://github.com/Org"), 773 | ) 774 | 775 | 776 | # Here's our "unit tests". 777 | class UrlParseTestCase(unittest.TestCase): 778 | def _test_valid(self, url, expected): 779 | p = parse(url) 780 | self.assertTrue(p.valid, "%s is not a valid URL" % url) 781 | for k, v in expected.items(): 782 | attr_v = getattr(p, k) 783 | self.assertEqual(attr_v, v, "[{}] Property '{}' should be '{}' but is '{}'".format(url, k, v, attr_v)) 784 | 785 | def test_valid_urls(self): 786 | for _test_type, data in VALID_PARSE_URLS: 787 | self._test_valid(*data) 788 | 789 | def _test_invalid(self, url): 790 | p = parse(url) 791 | self.assertFalse(p.valid) 792 | 793 | def test_invalid_urls(self): 794 | for _problem, url in INVALID_PARSE_URLS: 795 | self._test_invalid(url) 796 | 797 | 798 | # Test Suite 799 | suite = unittest.TestLoader().loadTestsFromTestCase(UrlParseTestCase) 800 | -------------------------------------------------------------------------------- /giturlparse/tests/test_rewrite.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from giturlparse import parse 4 | 5 | # Test data 6 | REWRITE_URLS = ( 7 | # GitHub SSH 8 | ("git@github.com:Org/Repo.git", "ssh", "git@github.com:Org/Repo.git"), 9 | ("git@github.com:Org/Repo.git/", "ssh", "git@github.com:Org/Repo.git"), 10 | ("git@github.com/Org/Repo", "ssh", "git@github.com:Org/Repo.git"), 11 | ("git@github.com/Org/Repo/", "ssh", "git@github.com:Org/Repo.git"), 12 | ("git@github.com:Org/Repo.git", "https", "https://github.com/Org/Repo.git"), 13 | ("git@github.com:Org/Repo.git", "git", "git://github.com/Org/Repo.git"), 14 | # GitHub HTTPS 15 | ("https://github.com/Org/Repo.git", "ssh", "git@github.com:Org/Repo.git"), 16 | ("https://github.com/Org/Repo.git", "https", "https://github.com/Org/Repo.git"), 17 | ("https://github.com/Org/Repo.git", "git", "git://github.com/Org/Repo.git"), 18 | # GitHub GIT 19 | ("git://github.com/Org/Repo.git", "ssh", "git@github.com:Org/Repo.git"), 20 | ("git://github.com/Org/Repo.git", "https", "https://github.com/Org/Repo.git"), 21 | ("git://github.com/Org/Repo.git", "git", "git://github.com/Org/Repo.git"), 22 | ( 23 | "git://github.com/Org/Repo/blob/master/dir/subdir/path", 24 | "git", 25 | "git://github.com/Org/Repo.git/blob/master/dir/subdir/path", 26 | ), 27 | # BitBucket SSH 28 | ("git@bitbucket.org:Org/Repo.git", "ssh", "git@bitbucket.org:Org/Repo.git"), 29 | ("git@bitbucket.org:Org/Repo.git", "https", "https://Org@bitbucket.org/Org/Repo.git"), 30 | # BitBucket HTTPS 31 | ("https://Org@bitbucket.org/Org/Repo.git", "ssh", "git@bitbucket.org:Org/Repo.git"), 32 | ("https://Org@bitbucket.org/Org/Repo.git", "https", "https://Org@bitbucket.org/Org/Repo.git"), 33 | # Assembla GIT 34 | ("git://git.assembla.com/SomeRepoID.git", "ssh", "git@git.assembla.com:SomeRepoID.git"), 35 | ("git://git.assembla.com/SomeRepoID.git", "git", "git://git.assembla.com/SomeRepoID.git"), 36 | # Assembla SSH 37 | ("git@git.assembla.com:SomeRepoID.git", "ssh", "git@git.assembla.com:SomeRepoID.git"), 38 | ("git@git.assembla.com:SomeRepoID.git", "git", "git://git.assembla.com/SomeRepoID.git"), 39 | # FriendCode HTTPS 40 | ("https://friendco.de/Aaron@user/test-repo.git", "https", "https://friendco.de/Aaron@user/test-repo.git"), 41 | # Generic 42 | ("git://git.buildroot.net/buildroot", "https", "https://git.buildroot.net/buildroot.git"), 43 | ("https://git.buildroot.net/buildroot", "git", "git://git.buildroot.net/buildroot.git"), 44 | ("https://git.buildroot.net/buildroot", "ssh", "git@git.buildroot.net:buildroot.git"), 45 | # Gitlab SSH 46 | ("git@host.org:Org/Repo.git", "ssh", "git@host.org:Org/Repo.git"), 47 | ("git@host.org:9999/Org/Repo.git", "ssh", "git@host.org:9999/Org/Repo.git"), 48 | ("git@host.org:Org/Repo.git", "https", "https://host.org/Org/Repo.git"), 49 | ("git@host.org:9999/Org/Repo.git", "https", "https://host.org/Org/Repo.git"), 50 | # Gitlab HTTPS 51 | ("https://host.org/Org/Repo.git", "ssh", "git@host.org:Org/Repo.git"), 52 | ("https://host.org/Org/Repo.git", "https", "https://host.org/Org/Repo.git"), 53 | ("https://host.org/Org/Group/Repo.git", "ssh", "git@host.org:Org/Group/Repo.git"), 54 | ( 55 | "https://host.org/Org/Group/Repo/-/blob/master/file.py", 56 | "ssh", 57 | "git@host.org:Org/Group/Repo.git/-/blob/master/file.py", 58 | ), 59 | ( 60 | "https://host.org/Org/Group/Repo/blob/master/file.py", 61 | "ssh", 62 | "git@host.org:Org/Group/Repo.git/blob/master/file.py", 63 | ), 64 | ) 65 | 66 | INVALID_PARSE_URLS = ( 67 | ("SSH Bad Username", "gitx@github.com:Org/Repo.git"), 68 | ("SSH No Repo", "git@github.com:Org"), 69 | ("HTTPS No Repo", "https://github.com/Org"), 70 | ("GIT No Repo", "git://github.com/Org"), 71 | ) 72 | 73 | 74 | class UrlRewriteTestCase(unittest.TestCase): 75 | def _test_rewrite(self, source, protocol, expected): 76 | parsed = parse(source) 77 | self.assertTrue(parsed.valid, "Invalid Url: %s" % source) 78 | return self.assertEqual(parse(source).format(protocol), expected) 79 | 80 | def test_rewrites(self): 81 | for data in REWRITE_URLS: 82 | self._test_rewrite(*data) 83 | 84 | 85 | # Test Suite 86 | suite = unittest.TestLoader().loadTestsFromTestCase(UrlRewriteTestCase) 87 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=40.6.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 119 7 | target-version = ["py310"] 8 | include = 'giturlparse/*py' 9 | 10 | [tool.towncrier] 11 | package = "giturlparse" 12 | directory = "changes" 13 | filename = "HISTORY.rst" 14 | title_format = "{version} ({project_date})" 15 | 16 | [tool.interrogate] 17 | ignore-init-method = true 18 | ignore-init-module = true 19 | ignore-magic = false 20 | ignore-semiprivate = false 21 | ignore-private = false 22 | ignore-module = true 23 | ignore-nested-functions = true 24 | fail-under = 0 25 | exclude = ["docs", ".tox"] 26 | ignore-regex = ["^get$", "^mock_.*", ".*BaseClass.*"] 27 | verbose = 0 28 | quiet = false 29 | whitelist-regex = [] 30 | color = true 31 | 32 | [tool.isort] 33 | profile = "black" 34 | combine_as_imports = true 35 | default_section = "THIRDPARTY" 36 | force_grid_wrap = 0 37 | include_trailing_comma = true 38 | known_first_party = "giturlparse" 39 | line_length = 119 40 | multi_line_output = 3 41 | use_parentheses = true 42 | 43 | [tool.ruff] 44 | ignore = [] 45 | line-length = 119 46 | target-version = "py310" 47 | 48 | [tool.ruff.mccabe] 49 | max-complexity = 10 50 | 51 | [tool.bumpversion] 52 | allow_dirty = false 53 | commit = true 54 | message = "Release {new_version}" 55 | commit_args = "--no-verify" 56 | tag = false 57 | current_version = "0.12.0" 58 | parse = """(?x) 59 | (?P[0-9]+) 60 | \\.(?P[0-9]+) 61 | \\.(?P[0-9]+) 62 | (?: 63 | .(?Pdev) 64 | (?:(?P[0-9]+))? 65 | )? 66 | """ 67 | serialize = [ 68 | "{major}.{minor}.{patch}.{release}{relver}", 69 | "{major}.{minor}.{patch}" 70 | ] 71 | 72 | [tool.bumpversion.parts.release] 73 | values = [ 74 | "dev", 75 | "" 76 | ] 77 | optional_value = "dev" 78 | 79 | [[tool.bumpversion.files]] 80 | filename = "giturlparse/__init__.py" 81 | search = "{current_version}" 82 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | coverage 3 | coveralls 4 | mock>=1.0.1 5 | tox 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = giturlparse 3 | version = attr: giturlparse.__version__ 4 | url = https://github.com/nephila/giturlparse 5 | author = Aaron O Mullan 6 | author_email = aaron@friendco.de 7 | maintainer = Iacopo Spalletti 8 | maintainer_email = i.spalletti@nephila.it 9 | description = A Git URL parsing module (supports parsing and rewriting) 10 | long_description = file: README.rst, HISTORY.rst 11 | long_description_content_type = text/x-rst 12 | license = Apache v2 13 | license_file = LICENSE 14 | keywords = giturlparse 15 | classifiers = 16 | Development Status :: 5 - Production/Stable 17 | Framework :: Django 18 | Intended Audience :: Developers 19 | License :: OSI Approved :: Apache Software License 20 | Natural Language :: English 21 | Programming Language :: Python :: 3.8 22 | Programming Language :: Python :: 3.9 23 | Programming Language :: Python :: 3.10 24 | Programming Language :: Python :: 3.11 25 | 26 | [options] 27 | include_package_data = True 28 | setup_requires = 29 | setuptools 30 | packages = giturlparse, giturlparse.platforms 31 | python_requires = >=3.8 32 | zip_safe = False 33 | test_suite = giturlparse.tests 34 | 35 | [options.package_data] 36 | * = *.txt, *.rst 37 | giturlparse = *.html *.png *.gif *js *jpg *jpeg *svg *py *mo *po 38 | 39 | [bdist_wheel] 40 | universal = 1 41 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup() 4 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import re 4 | import sys 5 | from glob import glob 6 | 7 | from invoke import task 8 | 9 | #: branch prefixes for which some checks are skipped 10 | SPECIAL_BRANCHES = ("master", "develop", "release") 11 | 12 | 13 | @task 14 | def clean(c): 15 | """Remove artifacts and binary files.""" 16 | c.run("python setup.py clean --all") 17 | patterns = ["build", "dist"] 18 | patterns.extend(glob("*.egg*")) 19 | patterns.append("docs/_build") 20 | patterns.append("**/*.pyc") 21 | for pattern in patterns: 22 | c.run("rm -rf {}".format(pattern)) 23 | 24 | 25 | @task 26 | def lint(c): 27 | """Run linting tox environments.""" 28 | c.run("tox -eruff,isort,black,pypi-description") 29 | 30 | 31 | @task # NOQA 32 | def format(c): # NOQA 33 | """Run code formatting tasks.""" 34 | c.run("tox -eblacken,isort_format") 35 | 36 | 37 | @task 38 | def towncrier_check(c): # NOQA 39 | """Check towncrier files.""" 40 | output = io.StringIO() 41 | c.run("git branch -a --contains HEAD", out_stream=output) 42 | skipped_branch_prefix = ["pull/", "release/", "develop", "master", "HEAD"] 43 | # cleanup branch names by removing PR-only names in local, remote and disconnected branches to ensure the current 44 | # (i.e. user defined) branch name is used 45 | branches = list( 46 | filter( 47 | lambda x: x and all(not x.startswith(part) for part in skipped_branch_prefix), 48 | ( 49 | branch.replace("origin/", "").replace("remotes/", "").strip("* (") 50 | for branch in output.getvalue().split("\n") 51 | ), 52 | ) 53 | ) 54 | print("Candidate branches", ", ".join(output.getvalue().split("\n"))) 55 | if not branches: 56 | # if no branch name matches, we are in one of the excluded branches above, so we just exit 57 | print("Skip check, branch excluded by configuration") 58 | return 59 | branch = branches[0] 60 | towncrier_file = None 61 | for branch in branches: 62 | if any(branch.startswith(prefix) for prefix in SPECIAL_BRANCHES): 63 | sys.exit(0) 64 | try: 65 | parts = re.search(r"(?P\w+)/\D*(?P\d+)\D*", branch).groups() 66 | towncrier_file = os.path.join("changes", "{1}.{0}".format(*parts)) 67 | if not os.path.exists(towncrier_file) or os.path.getsize(towncrier_file) == 0: 68 | print( 69 | "=========================\n" 70 | "Current tree does not contain the towncrier file {} or file is empty\n" 71 | "please check CONTRIBUTING documentation.\n" 72 | "=========================" 73 | "".format(towncrier_file) 74 | ) 75 | sys.exit(2) 76 | else: 77 | break 78 | except AttributeError: 79 | pass 80 | if not towncrier_file: 81 | print( 82 | "=========================\n" 83 | "Branch {} does not respect the '/(-)-description' format\n" 84 | "=========================\n" 85 | "".format(branch) 86 | ) 87 | sys.exit(1) 88 | 89 | 90 | @task 91 | def test(c): 92 | """Run test in local environment.""" 93 | c.run("python -munittest") 94 | 95 | 96 | @task 97 | def test_all(c): 98 | """Run all tox environments.""" 99 | c.run("tox") 100 | 101 | 102 | @task 103 | def coverage(c): 104 | """Run test with coverage in local environment.""" 105 | c.run("coverage erase") 106 | c.run("coverage run -m unittest") 107 | c.run("coverage report -m") 108 | 109 | 110 | @task 111 | def tag_release(c, level, new_version=""): 112 | """Tag release version.""" 113 | if new_version: 114 | new_version = f" --new-version {new_version}" 115 | c.run(f"bump-my-version bump {level}{new_version}") 116 | 117 | 118 | @task 119 | def tag_dev(c, level, new_version=""): 120 | """Tag development version.""" 121 | if new_version: 122 | new_version = f" --new-version {new_version}" 123 | elif level == "release": 124 | c.run("bump-my-version bump patch --no-commit") 125 | level = "relver" 126 | c.run(f"bump-my-version bump {level} --message='Bump develop version [ci skip]' {new_version} --allow-dirty") 127 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | black 4 | blacken 5 | isort 6 | isort_format 7 | ruff 8 | pypi-description 9 | towncrier 10 | py{311,310,39,38} 11 | skip_missing_interpreters = true 12 | 13 | [testenv] 14 | commands = {env:COMMAND:python} -m unittest 15 | deps = 16 | -r{toxinidir}/requirements-test.txt 17 | passenv = 18 | COMMAND 19 | PYTEST_* 20 | 21 | [testenv:ruff] 22 | commands = 23 | {envpython} -m ruff check giturlparse {posargs} 24 | {envpython} -minterrogate -c pyproject.toml giturlparse 25 | deps = 26 | interrogate 27 | ruff 28 | skip_install = true 29 | 30 | [testenv:isort] 31 | commands = 32 | {envpython} -m isort -c --df giturlparse 33 | deps = isort>5.9,<6 34 | skip_install = true 35 | 36 | [testenv:isort_format] 37 | commands = 38 | {envpython} -m isort giturlparse 39 | deps = {[testenv:isort]deps} 40 | skip_install = true 41 | 42 | [testenv:black] 43 | commands = 44 | {envpython} -m black --check --diff . 45 | deps = black 46 | skip_install = true 47 | 48 | [testenv:blacken] 49 | commands = 50 | {envpython} -m black . 51 | deps = {[testenv:black]deps} 52 | skip_install = true 53 | 54 | [testenv:towncrier] 55 | commands = 56 | {envpython} -m invoke towncrier-check 57 | deps = 58 | invoke 59 | skip_install = true 60 | 61 | [testenv:pypi-description] 62 | commands = 63 | {envpython} -m invoke clean 64 | {envpython} -m check_manifest 65 | {envpython} -m pep517.build . 66 | {envpython} -m twine check dist/* 67 | deps = 68 | invoke 69 | check-manifest 70 | pep517 71 | twine 72 | skip_install = true 73 | 74 | [testenv:release] 75 | commands = 76 | {envpython} -m invoke clean 77 | {envpython} -m check_manifest 78 | {envpython} -m pep517.build . 79 | {envpython} -m twine upload {posargs} dist/* 80 | deps = {[testenv:pypi-description]deps} 81 | passenv = 82 | TWINE_* 83 | skip_install = true 84 | 85 | [check-manifest] 86 | ignore = 87 | .* 88 | *.ini 89 | *.toml 90 | *.json 91 | *.txt 92 | *.yml 93 | *.yaml 94 | .tx/** 95 | changes/** 96 | docs/** 97 | cms_helper.py 98 | aldryn_config.py 99 | tasks.py 100 | tests/** 101 | *.mo 102 | ignore-bad-ideas = 103 | *.mo 104 | --------------------------------------------------------------------------------