├── .commitlintrc.js ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── config.yml ├── actions │ ├── create-check │ │ └── action.yml │ └── install-latest-npm │ │ └── action.yml ├── dependabot.yml ├── matchers │ └── tap.json ├── settings.yml └── workflows │ ├── audit.yml │ ├── ci-release.yml │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── post-dependabot.yml │ ├── pull-request.yml │ ├── release-integration.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .release-please-manifest.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── lib ├── from-url.js ├── hosts.js ├── index.js └── parse-url.js ├── package.json ├── release-please-config.json └── test ├── bitbucket.js ├── file.js ├── gist.js ├── github.js ├── gitlab.js ├── invalid.js ├── localhost.js ├── parse-url.js └── sourcehut.js /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | /* This file is automatically added by @npmcli/template-oss. Do not edit. */ 2 | 3 | module.exports = { 4 | extends: ['@commitlint/config-conventional'], 5 | rules: { 6 | 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'deps', 'chore']], 7 | 'header-max-length': [2, 'always', 80], 8 | 'subject-case': [0], 9 | 'body-max-line-length': [0], 10 | 'footer-max-line-length': [0], 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* This file is automatically added by @npmcli/template-oss. Do not edit. */ 2 | 3 | 'use strict' 4 | 5 | const { readdirSync: readdir } = require('fs') 6 | 7 | const localConfigs = readdir(__dirname) 8 | .filter((file) => file.startsWith('.eslintrc.local.')) 9 | .map((file) => `./${file}`) 10 | 11 | module.exports = { 12 | root: true, 13 | ignorePatterns: [ 14 | 'tap-testdir*/', 15 | ], 16 | extends: [ 17 | '@npmcli', 18 | ...localConfigs, 19 | ], 20 | } 21 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | * @npm/cli-team 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Bug 4 | description: File a bug/issue 5 | title: "[BUG] " 6 | labels: [ Bug, Needs Triage ] 7 | 8 | body: 9 | - type: checkboxes 10 | attributes: 11 | label: Is there an existing issue for this? 12 | description: Please [search here](./issues) to see if an issue already exists for your problem. 13 | options: 14 | - label: I have searched the existing issues 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Current Behavior 19 | description: A clear & concise description of what you're experiencing. 20 | validations: 21 | required: false 22 | - type: textarea 23 | attributes: 24 | label: Expected Behavior 25 | description: A clear & concise description of what you expected to happen. 26 | validations: 27 | required: false 28 | - type: textarea 29 | attributes: 30 | label: Steps To Reproduce 31 | description: Steps to reproduce the behavior. 32 | value: | 33 | 1. In this environment... 34 | 2. With this config... 35 | 3. Run '...' 36 | 4. See error... 37 | validations: 38 | required: false 39 | - type: textarea 40 | attributes: 41 | label: Environment 42 | description: | 43 | examples: 44 | - **npm**: 7.6.3 45 | - **Node**: 13.14.0 46 | - **OS**: Ubuntu 20.04 47 | - **platform**: Macbook Pro 48 | value: | 49 | - npm: 50 | - Node: 51 | - OS: 52 | - platform: 53 | validations: 54 | required: false 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | blank_issues_enabled: true 4 | -------------------------------------------------------------------------------- /.github/actions/create-check/action.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: 'Create Check' 4 | inputs: 5 | name: 6 | required: true 7 | token: 8 | required: true 9 | sha: 10 | required: true 11 | check-name: 12 | default: '' 13 | outputs: 14 | check-id: 15 | value: ${{ steps.create-check.outputs.check_id }} 16 | runs: 17 | using: "composite" 18 | steps: 19 | - name: Get Workflow Job 20 | uses: actions/github-script@v7 21 | id: workflow 22 | env: 23 | JOB_NAME: "${{ inputs.name }}" 24 | SHA: "${{ inputs.sha }}" 25 | with: 26 | result-encoding: string 27 | script: | 28 | const { repo: { owner, repo}, runId, serverUrl } = context 29 | const { JOB_NAME, SHA } = process.env 30 | 31 | const job = await github.rest.actions.listJobsForWorkflowRun({ 32 | owner, 33 | repo, 34 | run_id: runId, 35 | per_page: 100 36 | }).then(r => r.data.jobs.find(j => j.name.endsWith(JOB_NAME))) 37 | 38 | return [ 39 | `This check is assosciated with ${serverUrl}/${owner}/${repo}/commit/${SHA}.`, 40 | 'Run logs:', 41 | job?.html_url || `could not be found for a job ending with: "${JOB_NAME}"`, 42 | ].join(' ') 43 | - name: Create Check 44 | uses: LouisBrunner/checks-action@v1.6.0 45 | id: create-check 46 | with: 47 | token: ${{ inputs.token }} 48 | sha: ${{ inputs.sha }} 49 | status: in_progress 50 | name: ${{ inputs.check-name || inputs.name }} 51 | output: | 52 | {"summary":"${{ steps.workflow.outputs.result }}"} 53 | -------------------------------------------------------------------------------- /.github/actions/install-latest-npm/action.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: 'Install Latest npm' 4 | description: 'Install the latest version of npm compatible with the Node version' 5 | inputs: 6 | node: 7 | description: 'Current Node version' 8 | required: true 9 | runs: 10 | using: "composite" 11 | steps: 12 | # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows 13 | - name: Update Windows npm 14 | if: | 15 | runner.os == 'Windows' && ( 16 | startsWith(inputs.node, 'v10.') || 17 | startsWith(inputs.node, 'v12.') || 18 | startsWith(inputs.node, 'v14.') 19 | ) 20 | shell: cmd 21 | run: | 22 | curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz 23 | tar xf npm-7.5.4.tgz 24 | cd package 25 | node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz 26 | cd .. 27 | rmdir /s /q package 28 | - name: Install Latest npm 29 | shell: bash 30 | env: 31 | NODE_VERSION: ${{ inputs.node }} 32 | working-directory: ${{ runner.temp }} 33 | run: | 34 | MATCH="" 35 | SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") 36 | 37 | echo "node@$NODE_VERSION" 38 | 39 | for SPEC in ${SPECS[@]}; do 40 | ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') 41 | echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" 42 | 43 | if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then 44 | MATCH=$SPEC 45 | echo "Found compatible version: npm@$MATCH" 46 | break 47 | fi 48 | done 49 | 50 | if [ -z $MATCH ]; then 51 | echo "Could not find a compatible version of npm for node@$NODE_VERSION" 52 | exit 1 53 | fi 54 | 55 | npm i --prefer-online --no-fund --no-audit -g npm@$MATCH 56 | - name: npm Version 57 | shell: bash 58 | run: npm -v 59 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: npm 7 | directory: / 8 | schedule: 9 | interval: daily 10 | target-branch: "main" 11 | allow: 12 | - dependency-type: direct 13 | versioning-strategy: increase-if-necessary 14 | commit-message: 15 | prefix: deps 16 | prefix-development: chore 17 | labels: 18 | - "Dependencies" 19 | open-pull-requests-limit: 10 20 | - package-ecosystem: npm 21 | directory: / 22 | schedule: 23 | interval: daily 24 | target-branch: "release/v5" 25 | allow: 26 | - dependency-type: direct 27 | dependency-name: "@npmcli/template-oss" 28 | versioning-strategy: increase-if-necessary 29 | commit-message: 30 | prefix: deps 31 | prefix-development: chore 32 | labels: 33 | - "Dependencies" 34 | - "Backport" 35 | - "release/v5" 36 | open-pull-requests-limit: 10 37 | - package-ecosystem: npm 38 | directory: / 39 | schedule: 40 | interval: daily 41 | target-branch: "release/v6" 42 | allow: 43 | - dependency-type: direct 44 | dependency-name: "@npmcli/template-oss" 45 | versioning-strategy: increase-if-necessary 46 | commit-message: 47 | prefix: deps 48 | prefix-development: chore 49 | labels: 50 | - "Dependencies" 51 | - "Backport" 52 | - "release/v6" 53 | open-pull-requests-limit: 10 54 | -------------------------------------------------------------------------------- /.github/matchers/tap.json: -------------------------------------------------------------------------------- 1 | { 2 | "//@npmcli/template-oss": "This file is automatically added by @npmcli/template-oss. Do not edit.", 3 | "problemMatcher": [ 4 | { 5 | "owner": "tap", 6 | "pattern": [ 7 | { 8 | "regexp": "^\\s*not ok \\d+ - (.*)", 9 | "message": 1 10 | }, 11 | { 12 | "regexp": "^\\s*---" 13 | }, 14 | { 15 | "regexp": "^\\s*at:" 16 | }, 17 | { 18 | "regexp": "^\\s*line:\\s*(\\d+)", 19 | "line": 1 20 | }, 21 | { 22 | "regexp": "^\\s*column:\\s*(\\d+)", 23 | "column": 1 24 | }, 25 | { 26 | "regexp": "^\\s*file:\\s*(.*)", 27 | "file": 1 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | repository: 4 | allow_merge_commit: false 5 | allow_rebase_merge: true 6 | allow_squash_merge: true 7 | squash_merge_commit_title: PR_TITLE 8 | squash_merge_commit_message: PR_BODY 9 | delete_branch_on_merge: true 10 | enable_automated_security_fixes: true 11 | enable_vulnerability_alerts: true 12 | 13 | branches: 14 | - name: main 15 | protection: 16 | required_status_checks: null 17 | enforce_admins: true 18 | block_creations: true 19 | required_pull_request_reviews: 20 | required_approving_review_count: 1 21 | require_code_owner_reviews: true 22 | require_last_push_approval: true 23 | dismiss_stale_reviews: true 24 | restrictions: 25 | apps: [] 26 | users: [] 27 | teams: [ "cli-team" ] 28 | - name: release/v5 29 | protection: 30 | required_status_checks: null 31 | enforce_admins: true 32 | block_creations: true 33 | required_pull_request_reviews: 34 | required_approving_review_count: 1 35 | require_code_owner_reviews: true 36 | require_last_push_approval: true 37 | dismiss_stale_reviews: true 38 | restrictions: 39 | apps: [] 40 | users: [] 41 | teams: [ "cli-team" ] 42 | - name: release/v6 43 | protection: 44 | required_status_checks: null 45 | enforce_admins: true 46 | block_creations: true 47 | required_pull_request_reviews: 48 | required_approving_review_count: 1 49 | require_code_owner_reviews: true 50 | require_last_push_approval: true 51 | dismiss_stale_reviews: true 52 | restrictions: 53 | apps: [] 54 | users: [] 55 | teams: [ "cli-team" ] 56 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Audit 4 | 5 | on: 6 | workflow_dispatch: 7 | schedule: 8 | # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1 9 | - cron: "0 8 * * 1" 10 | 11 | jobs: 12 | audit: 13 | name: Audit Dependencies 14 | if: github.repository_owner == 'npm' 15 | runs-on: ubuntu-latest 16 | defaults: 17 | run: 18 | shell: bash 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | - name: Setup Git User 23 | run: | 24 | git config --global user.email "npm-cli+bot@github.com" 25 | git config --global user.name "npm CLI robot" 26 | - name: Setup Node 27 | uses: actions/setup-node@v4 28 | id: node 29 | with: 30 | node-version: 22.x 31 | check-latest: contains('22.x', '.x') 32 | - name: Install Latest npm 33 | uses: ./.github/actions/install-latest-npm 34 | with: 35 | node: ${{ steps.node.outputs.node-version }} 36 | - name: Install Dependencies 37 | run: npm i --ignore-scripts --no-audit --no-fund --package-lock 38 | - name: Run Production Audit 39 | run: npm audit --omit=dev 40 | - name: Run Full Audit 41 | run: npm audit --audit-level=none 42 | -------------------------------------------------------------------------------- /.github/workflows/ci-release.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CI - Release 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | ref: 9 | required: true 10 | type: string 11 | default: main 12 | workflow_call: 13 | inputs: 14 | ref: 15 | required: true 16 | type: string 17 | check-sha: 18 | required: true 19 | type: string 20 | 21 | jobs: 22 | lint-all: 23 | name: Lint All 24 | if: github.repository_owner == 'npm' 25 | runs-on: ubuntu-latest 26 | defaults: 27 | run: 28 | shell: bash 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | with: 33 | ref: ${{ inputs.ref }} 34 | - name: Setup Git User 35 | run: | 36 | git config --global user.email "npm-cli+bot@github.com" 37 | git config --global user.name "npm CLI robot" 38 | - name: Create Check 39 | id: create-check 40 | if: ${{ inputs.check-sha }} 41 | uses: ./.github/actions/create-check 42 | with: 43 | name: "Lint All" 44 | token: ${{ secrets.GITHUB_TOKEN }} 45 | sha: ${{ inputs.check-sha }} 46 | - name: Setup Node 47 | uses: actions/setup-node@v4 48 | id: node 49 | with: 50 | node-version: 22.x 51 | check-latest: contains('22.x', '.x') 52 | - name: Install Latest npm 53 | uses: ./.github/actions/install-latest-npm 54 | with: 55 | node: ${{ steps.node.outputs.node-version }} 56 | - name: Install Dependencies 57 | run: npm i --ignore-scripts --no-audit --no-fund 58 | - name: Lint 59 | run: npm run lint --ignore-scripts 60 | - name: Post Lint 61 | run: npm run postlint --ignore-scripts 62 | - name: Conclude Check 63 | uses: LouisBrunner/checks-action@v1.6.0 64 | if: steps.create-check.outputs.check-id && always() 65 | with: 66 | token: ${{ secrets.GITHUB_TOKEN }} 67 | conclusion: ${{ job.status }} 68 | check_id: ${{ steps.create-check.outputs.check-id }} 69 | 70 | test-all: 71 | name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }} 72 | if: github.repository_owner == 'npm' 73 | strategy: 74 | fail-fast: false 75 | matrix: 76 | platform: 77 | - name: Linux 78 | os: ubuntu-latest 79 | shell: bash 80 | - name: macOS 81 | os: macos-latest 82 | shell: bash 83 | - name: macOS 84 | os: macos-13 85 | shell: bash 86 | - name: Windows 87 | os: windows-latest 88 | shell: cmd 89 | node-version: 90 | - 18.17.0 91 | - 18.x 92 | - 20.5.0 93 | - 20.x 94 | - 22.x 95 | exclude: 96 | - platform: { name: macOS, os: macos-13, shell: bash } 97 | node-version: 18.17.0 98 | - platform: { name: macOS, os: macos-13, shell: bash } 99 | node-version: 18.x 100 | - platform: { name: macOS, os: macos-13, shell: bash } 101 | node-version: 20.5.0 102 | - platform: { name: macOS, os: macos-13, shell: bash } 103 | node-version: 20.x 104 | - platform: { name: macOS, os: macos-13, shell: bash } 105 | node-version: 22.x 106 | runs-on: ${{ matrix.platform.os }} 107 | defaults: 108 | run: 109 | shell: ${{ matrix.platform.shell }} 110 | steps: 111 | - name: Checkout 112 | uses: actions/checkout@v4 113 | with: 114 | ref: ${{ inputs.ref }} 115 | - name: Setup Git User 116 | run: | 117 | git config --global user.email "npm-cli+bot@github.com" 118 | git config --global user.name "npm CLI robot" 119 | - name: Create Check 120 | id: create-check 121 | if: ${{ inputs.check-sha }} 122 | uses: ./.github/actions/create-check 123 | with: 124 | name: "Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}" 125 | token: ${{ secrets.GITHUB_TOKEN }} 126 | sha: ${{ inputs.check-sha }} 127 | - name: Setup Node 128 | uses: actions/setup-node@v4 129 | id: node 130 | with: 131 | node-version: ${{ matrix.node-version }} 132 | check-latest: contains(matrix.node-version, '.x') 133 | - name: Install Latest npm 134 | uses: ./.github/actions/install-latest-npm 135 | with: 136 | node: ${{ steps.node.outputs.node-version }} 137 | - name: Install Dependencies 138 | run: npm i --ignore-scripts --no-audit --no-fund 139 | - name: Add Problem Matcher 140 | run: echo "::add-matcher::.github/matchers/tap.json" 141 | - name: Test 142 | run: npm test --ignore-scripts 143 | - name: Conclude Check 144 | uses: LouisBrunner/checks-action@v1.6.0 145 | if: steps.create-check.outputs.check-id && always() 146 | with: 147 | token: ${{ secrets.GITHUB_TOKEN }} 148 | conclusion: ${{ job.status }} 149 | check_id: ${{ steps.create-check.outputs.check-id }} 150 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CI 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | - release/v* 12 | schedule: 13 | # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 14 | - cron: "0 9 * * 1" 15 | 16 | jobs: 17 | lint: 18 | name: Lint 19 | if: github.repository_owner == 'npm' 20 | runs-on: ubuntu-latest 21 | defaults: 22 | run: 23 | shell: bash 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | - name: Setup Git User 28 | run: | 29 | git config --global user.email "npm-cli+bot@github.com" 30 | git config --global user.name "npm CLI robot" 31 | - name: Setup Node 32 | uses: actions/setup-node@v4 33 | id: node 34 | with: 35 | node-version: 22.x 36 | check-latest: contains('22.x', '.x') 37 | - name: Install Latest npm 38 | uses: ./.github/actions/install-latest-npm 39 | with: 40 | node: ${{ steps.node.outputs.node-version }} 41 | - name: Install Dependencies 42 | run: npm i --ignore-scripts --no-audit --no-fund 43 | - name: Lint 44 | run: npm run lint --ignore-scripts 45 | - name: Post Lint 46 | run: npm run postlint --ignore-scripts 47 | 48 | test: 49 | name: Test - ${{ matrix.platform.name }} - ${{ matrix.node-version }} 50 | if: github.repository_owner == 'npm' 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | platform: 55 | - name: Linux 56 | os: ubuntu-latest 57 | shell: bash 58 | - name: macOS 59 | os: macos-latest 60 | shell: bash 61 | - name: macOS 62 | os: macos-13 63 | shell: bash 64 | - name: Windows 65 | os: windows-latest 66 | shell: cmd 67 | node-version: 68 | - 18.17.0 69 | - 18.x 70 | - 20.5.0 71 | - 20.x 72 | - 22.x 73 | exclude: 74 | - platform: { name: macOS, os: macos-13, shell: bash } 75 | node-version: 18.17.0 76 | - platform: { name: macOS, os: macos-13, shell: bash } 77 | node-version: 18.x 78 | - platform: { name: macOS, os: macos-13, shell: bash } 79 | node-version: 20.5.0 80 | - platform: { name: macOS, os: macos-13, shell: bash } 81 | node-version: 20.x 82 | - platform: { name: macOS, os: macos-13, shell: bash } 83 | node-version: 22.x 84 | runs-on: ${{ matrix.platform.os }} 85 | defaults: 86 | run: 87 | shell: ${{ matrix.platform.shell }} 88 | steps: 89 | - name: Checkout 90 | uses: actions/checkout@v4 91 | - name: Setup Git User 92 | run: | 93 | git config --global user.email "npm-cli+bot@github.com" 94 | git config --global user.name "npm CLI robot" 95 | - name: Setup Node 96 | uses: actions/setup-node@v4 97 | id: node 98 | with: 99 | node-version: ${{ matrix.node-version }} 100 | check-latest: contains(matrix.node-version, '.x') 101 | - name: Install Latest npm 102 | uses: ./.github/actions/install-latest-npm 103 | with: 104 | node: ${{ steps.node.outputs.node-version }} 105 | - name: Install Dependencies 106 | run: npm i --ignore-scripts --no-audit --no-fund 107 | - name: Add Problem Matcher 108 | run: echo "::add-matcher::.github/matchers/tap.json" 109 | - name: Test 110 | run: npm test --ignore-scripts 111 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CodeQL 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | - release/v* 10 | pull_request: 11 | branches: 12 | - main 13 | - release/v* 14 | schedule: 15 | # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 16 | - cron: "0 10 * * 1" 17 | 18 | jobs: 19 | analyze: 20 | name: Analyze 21 | runs-on: ubuntu-latest 22 | permissions: 23 | actions: read 24 | contents: read 25 | security-events: write 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - name: Setup Git User 30 | run: | 31 | git config --global user.email "npm-cli+bot@github.com" 32 | git config --global user.name "npm CLI robot" 33 | - name: Initialize CodeQL 34 | uses: github/codeql-action/init@v3 35 | with: 36 | languages: javascript 37 | - name: Perform CodeQL Analysis 38 | uses: github/codeql-action/analyze@v3 39 | -------------------------------------------------------------------------------- /.github/workflows/post-dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Post Dependabot 4 | 5 | on: pull_request 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | template-oss: 12 | name: template-oss 13 | if: github.repository_owner == 'npm' && github.actor == 'dependabot[bot]' 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | shell: bash 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | ref: ${{ github.event.pull_request.head.ref }} 23 | - name: Setup Git User 24 | run: | 25 | git config --global user.email "npm-cli+bot@github.com" 26 | git config --global user.name "npm CLI robot" 27 | - name: Setup Node 28 | uses: actions/setup-node@v4 29 | id: node 30 | with: 31 | node-version: 22.x 32 | check-latest: contains('22.x', '.x') 33 | - name: Install Latest npm 34 | uses: ./.github/actions/install-latest-npm 35 | with: 36 | node: ${{ steps.node.outputs.node-version }} 37 | - name: Install Dependencies 38 | run: npm i --ignore-scripts --no-audit --no-fund 39 | - name: Fetch Dependabot Metadata 40 | id: metadata 41 | uses: dependabot/fetch-metadata@v1 42 | with: 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | # Dependabot can update multiple directories so we output which directory 46 | # it is acting on so we can run the command for the correct root or workspace 47 | - name: Get Dependabot Directory 48 | if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') 49 | id: flags 50 | run: | 51 | dependabot_dir="${{ steps.metadata.outputs.directory }}" 52 | if [[ "$dependabot_dir" == "/" || "$dependabot_dir" == "/main" ]]; then 53 | echo "workspace=-iwr" >> $GITHUB_OUTPUT 54 | else 55 | # strip leading slash from directory so it works as a 56 | # a path to the workspace flag 57 | echo "workspace=-w ${dependabot_dir#/}" >> $GITHUB_OUTPUT 58 | fi 59 | 60 | - name: Apply Changes 61 | if: steps.flags.outputs.workspace 62 | id: apply 63 | run: | 64 | npm run template-oss-apply ${{ steps.flags.outputs.workspace }} 65 | if [[ `git status --porcelain` ]]; then 66 | echo "changes=true" >> $GITHUB_OUTPUT 67 | fi 68 | # This only sets the conventional commit prefix. This workflow can't reliably determine 69 | # what the breaking change is though. If a BREAKING CHANGE message is required then 70 | # this PR check will fail and the commit will be amended with stafftools 71 | if [[ "${{ steps.metadata.outputs.update-type }}" == "version-update:semver-major" ]]; then 72 | prefix='feat!' 73 | else 74 | prefix='chore' 75 | fi 76 | echo "message=$prefix: postinstall for dependabot template-oss PR" >> $GITHUB_OUTPUT 77 | 78 | # This step will fail if template-oss has made any workflow updates. It is impossible 79 | # for a workflow to update other workflows. In the case it does fail, we continue 80 | # and then try to apply only a portion of the changes in the next step 81 | - name: Push All Changes 82 | if: steps.apply.outputs.changes 83 | id: push 84 | continue-on-error: true 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | run: | 88 | git commit -am "${{ steps.apply.outputs.message }}" 89 | git push 90 | 91 | # If the previous step failed, then reset the commit and remove any workflow changes 92 | # and attempt to commit and push again. This is helpful because we will have a commit 93 | # with the correct prefix that we can then --amend with @npmcli/stafftools later. 94 | - name: Push All Changes Except Workflows 95 | if: steps.apply.outputs.changes && steps.push.outcome == 'failure' 96 | env: 97 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 | run: | 99 | git reset HEAD~ 100 | git checkout HEAD -- .github/workflows/ 101 | git clean -fd .github/workflows/ 102 | git commit -am "${{ steps.apply.outputs.message }}" 103 | git push 104 | 105 | # Check if all the necessary template-oss changes were applied. Since we continued 106 | # on errors in one of the previous steps, this check will fail if our follow up 107 | # only applied a portion of the changes and we need to followup manually. 108 | # 109 | # Note that this used to run `lint` and `postlint` but that will fail this action 110 | # if we've also shipped any linting changes separate from template-oss. We do 111 | # linting in another action, so we want to fail this one only if there are 112 | # template-oss changes that could not be applied. 113 | - name: Check Changes 114 | if: steps.apply.outputs.changes 115 | run: | 116 | npm exec --offline ${{ steps.flags.outputs.workspace }} -- template-oss-check 117 | 118 | - name: Fail on Breaking Change 119 | if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!') 120 | run: | 121 | echo "This PR has a breaking change. Run 'npx -p @npmcli/stafftools gh template-oss-fix'" 122 | echo "for more information on how to fix this with a BREAKING CHANGE footer." 123 | exit 1 124 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Pull Request 4 | 5 | on: 6 | pull_request: 7 | types: 8 | - opened 9 | - reopened 10 | - edited 11 | - synchronize 12 | 13 | jobs: 14 | commitlint: 15 | name: Lint Commits 16 | if: github.repository_owner == 'npm' 17 | runs-on: ubuntu-latest 18 | defaults: 19 | run: 20 | shell: bash 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | - name: Setup Git User 27 | run: | 28 | git config --global user.email "npm-cli+bot@github.com" 29 | git config --global user.name "npm CLI robot" 30 | - name: Setup Node 31 | uses: actions/setup-node@v4 32 | id: node 33 | with: 34 | node-version: 22.x 35 | check-latest: contains('22.x', '.x') 36 | - name: Install Latest npm 37 | uses: ./.github/actions/install-latest-npm 38 | with: 39 | node: ${{ steps.node.outputs.node-version }} 40 | - name: Install Dependencies 41 | run: npm i --ignore-scripts --no-audit --no-fund 42 | - name: Run Commitlint on Commits 43 | id: commit 44 | continue-on-error: true 45 | run: npx --offline commitlint -V --from 'origin/${{ github.base_ref }}' --to ${{ github.event.pull_request.head.sha }} 46 | - name: Run Commitlint on PR Title 47 | if: steps.commit.outcome == 'failure' 48 | env: 49 | PR_TITLE: ${{ github.event.pull_request.title }} 50 | run: echo "$PR_TITLE" | npx --offline commitlint -V 51 | -------------------------------------------------------------------------------- /.github/workflows/release-integration.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Release Integration 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | releases: 9 | required: true 10 | type: string 11 | description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version' 12 | workflow_call: 13 | inputs: 14 | releases: 15 | required: true 16 | type: string 17 | description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version' 18 | secrets: 19 | PUBLISH_TOKEN: 20 | required: true 21 | 22 | jobs: 23 | publish: 24 | name: Publish 25 | runs-on: ubuntu-latest 26 | defaults: 27 | run: 28 | shell: bash 29 | permissions: 30 | id-token: write 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | with: 35 | ref: ${{ fromJSON(inputs.releases)[0].tagName }} 36 | - name: Setup Git User 37 | run: | 38 | git config --global user.email "npm-cli+bot@github.com" 39 | git config --global user.name "npm CLI robot" 40 | - name: Setup Node 41 | uses: actions/setup-node@v4 42 | id: node 43 | with: 44 | node-version: 22.x 45 | check-latest: contains('22.x', '.x') 46 | - name: Install Latest npm 47 | uses: ./.github/actions/install-latest-npm 48 | with: 49 | node: ${{ steps.node.outputs.node-version }} 50 | - name: Install Dependencies 51 | run: npm i --ignore-scripts --no-audit --no-fund 52 | - name: Set npm authToken 53 | run: npm config set '//registry.npmjs.org/:_authToken'=\${PUBLISH_TOKEN} 54 | - name: Publish 55 | env: 56 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 57 | RELEASES: ${{ inputs.releases }} 58 | run: | 59 | EXIT_CODE=0 60 | 61 | for release in $(echo $RELEASES | jq -r '.[] | @base64'); do 62 | PUBLISH_TAG=$(echo "$release" | base64 --decode | jq -r .publishTag) 63 | npm publish --provenance --tag="$PUBLISH_TAG" 64 | STATUS=$? 65 | if [[ "$STATUS" -eq 1 ]]; then 66 | EXIT_CODE=$STATUS 67 | fi 68 | done 69 | 70 | exit $EXIT_CODE 71 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Release 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | - release/v* 10 | 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | checks: write 15 | 16 | jobs: 17 | release: 18 | outputs: 19 | pr: ${{ steps.release.outputs.pr }} 20 | pr-branch: ${{ steps.release.outputs.pr-branch }} 21 | pr-number: ${{ steps.release.outputs.pr-number }} 22 | pr-sha: ${{ steps.release.outputs.pr-sha }} 23 | releases: ${{ steps.release.outputs.releases }} 24 | comment-id: ${{ steps.create-comment.outputs.comment-id || steps.update-comment.outputs.comment-id }} 25 | check-id: ${{ steps.create-check.outputs.check-id }} 26 | name: Release 27 | if: github.repository_owner == 'npm' 28 | runs-on: ubuntu-latest 29 | defaults: 30 | run: 31 | shell: bash 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | - name: Setup Git User 36 | run: | 37 | git config --global user.email "npm-cli+bot@github.com" 38 | git config --global user.name "npm CLI robot" 39 | - name: Setup Node 40 | uses: actions/setup-node@v4 41 | id: node 42 | with: 43 | node-version: 22.x 44 | check-latest: contains('22.x', '.x') 45 | - name: Install Latest npm 46 | uses: ./.github/actions/install-latest-npm 47 | with: 48 | node: ${{ steps.node.outputs.node-version }} 49 | - name: Install Dependencies 50 | run: npm i --ignore-scripts --no-audit --no-fund 51 | - name: Release Please 52 | id: release 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | run: npx --offline template-oss-release-please --branch="${{ github.ref_name }}" --backport="" --defaultTag="latest" 56 | - name: Create Release Manager Comment Text 57 | if: steps.release.outputs.pr-number 58 | uses: actions/github-script@v7 59 | id: comment-text 60 | with: 61 | result-encoding: string 62 | script: | 63 | const { runId, repo: { owner, repo } } = context 64 | const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId }) 65 | return['## Release Manager', `Release workflow run: ${workflow.html_url}`].join('\n\n') 66 | - name: Find Release Manager Comment 67 | uses: peter-evans/find-comment@v2 68 | if: steps.release.outputs.pr-number 69 | id: found-comment 70 | with: 71 | issue-number: ${{ steps.release.outputs.pr-number }} 72 | comment-author: 'github-actions[bot]' 73 | body-includes: '## Release Manager' 74 | - name: Create Release Manager Comment 75 | id: create-comment 76 | if: steps.release.outputs.pr-number && !steps.found-comment.outputs.comment-id 77 | uses: peter-evans/create-or-update-comment@v3 78 | with: 79 | issue-number: ${{ steps.release.outputs.pr-number }} 80 | body: ${{ steps.comment-text.outputs.result }} 81 | - name: Update Release Manager Comment 82 | id: update-comment 83 | if: steps.release.outputs.pr-number && steps.found-comment.outputs.comment-id 84 | uses: peter-evans/create-or-update-comment@v3 85 | with: 86 | comment-id: ${{ steps.found-comment.outputs.comment-id }} 87 | body: ${{ steps.comment-text.outputs.result }} 88 | edit-mode: 'replace' 89 | - name: Create Check 90 | id: create-check 91 | uses: ./.github/actions/create-check 92 | if: steps.release.outputs.pr-sha 93 | with: 94 | name: "Release" 95 | token: ${{ secrets.GITHUB_TOKEN }} 96 | sha: ${{ steps.release.outputs.pr-sha }} 97 | 98 | update: 99 | needs: release 100 | outputs: 101 | sha: ${{ steps.commit.outputs.sha }} 102 | check-id: ${{ steps.create-check.outputs.check-id }} 103 | name: Update - Release 104 | if: github.repository_owner == 'npm' && needs.release.outputs.pr 105 | runs-on: ubuntu-latest 106 | defaults: 107 | run: 108 | shell: bash 109 | steps: 110 | - name: Checkout 111 | uses: actions/checkout@v4 112 | with: 113 | fetch-depth: 0 114 | ref: ${{ needs.release.outputs.pr-branch }} 115 | - name: Setup Git User 116 | run: | 117 | git config --global user.email "npm-cli+bot@github.com" 118 | git config --global user.name "npm CLI robot" 119 | - name: Setup Node 120 | uses: actions/setup-node@v4 121 | id: node 122 | with: 123 | node-version: 22.x 124 | check-latest: contains('22.x', '.x') 125 | - name: Install Latest npm 126 | uses: ./.github/actions/install-latest-npm 127 | with: 128 | node: ${{ steps.node.outputs.node-version }} 129 | - name: Install Dependencies 130 | run: npm i --ignore-scripts --no-audit --no-fund 131 | - name: Create Release Manager Checklist Text 132 | id: comment-text 133 | env: 134 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 135 | run: npm exec --offline -- template-oss-release-manager --pr="${{ needs.release.outputs.pr-number }}" --backport="" --defaultTag="latest" --publish 136 | - name: Append Release Manager Comment 137 | uses: peter-evans/create-or-update-comment@v3 138 | with: 139 | comment-id: ${{ needs.release.outputs.comment-id }} 140 | body: ${{ steps.comment-text.outputs.result }} 141 | edit-mode: 'append' 142 | - name: Run Post Pull Request Actions 143 | env: 144 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 145 | run: npm run rp-pull-request --ignore-scripts --if-present -- --pr="${{ needs.release.outputs.pr-number }}" --commentId="${{ needs.release.outputs.comment-id }}" 146 | - name: Commit 147 | id: commit 148 | env: 149 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 150 | run: | 151 | git commit --all --amend --no-edit || true 152 | git push --force-with-lease 153 | echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 154 | - name: Create Check 155 | id: create-check 156 | uses: ./.github/actions/create-check 157 | with: 158 | name: "Update - Release" 159 | check-name: "Release" 160 | token: ${{ secrets.GITHUB_TOKEN }} 161 | sha: ${{ steps.commit.outputs.sha }} 162 | - name: Conclude Check 163 | uses: LouisBrunner/checks-action@v1.6.0 164 | with: 165 | token: ${{ secrets.GITHUB_TOKEN }} 166 | conclusion: ${{ job.status }} 167 | check_id: ${{ needs.release.outputs.check-id }} 168 | 169 | ci: 170 | name: CI - Release 171 | needs: [ release, update ] 172 | if: needs.release.outputs.pr 173 | uses: ./.github/workflows/ci-release.yml 174 | with: 175 | ref: ${{ needs.release.outputs.pr-branch }} 176 | check-sha: ${{ needs.update.outputs.sha }} 177 | 178 | post-ci: 179 | needs: [ release, update, ci ] 180 | name: Post CI - Release 181 | if: github.repository_owner == 'npm' && needs.release.outputs.pr && always() 182 | runs-on: ubuntu-latest 183 | defaults: 184 | run: 185 | shell: bash 186 | steps: 187 | - name: Get CI Conclusion 188 | id: conclusion 189 | run: | 190 | result="" 191 | if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then 192 | result="failure" 193 | elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then 194 | result="cancelled" 195 | else 196 | result="success" 197 | fi 198 | echo "result=$result" >> $GITHUB_OUTPUT 199 | - name: Conclude Check 200 | uses: LouisBrunner/checks-action@v1.6.0 201 | with: 202 | token: ${{ secrets.GITHUB_TOKEN }} 203 | conclusion: ${{ steps.conclusion.outputs.result }} 204 | check_id: ${{ needs.update.outputs.check-id }} 205 | 206 | post-release: 207 | needs: release 208 | outputs: 209 | comment-id: ${{ steps.create-comment.outputs.comment-id }} 210 | name: Post Release - Release 211 | if: github.repository_owner == 'npm' && needs.release.outputs.releases 212 | runs-on: ubuntu-latest 213 | defaults: 214 | run: 215 | shell: bash 216 | steps: 217 | - name: Create Release PR Comment Text 218 | id: comment-text 219 | uses: actions/github-script@v7 220 | env: 221 | RELEASES: ${{ needs.release.outputs.releases }} 222 | with: 223 | result-encoding: string 224 | script: | 225 | const releases = JSON.parse(process.env.RELEASES) 226 | const { runId, repo: { owner, repo } } = context 227 | const issue_number = releases[0].prNumber 228 | const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}` 229 | 230 | return [ 231 | '## Release Workflow\n', 232 | ...releases.map(r => `- \`${r.pkgName}@${r.version}\` ${r.url}`), 233 | `- Workflow run: :arrows_counterclockwise: ${runUrl}`, 234 | ].join('\n') 235 | - name: Create Release PR Comment 236 | id: create-comment 237 | uses: peter-evans/create-or-update-comment@v3 238 | with: 239 | issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }} 240 | body: ${{ steps.comment-text.outputs.result }} 241 | 242 | release-integration: 243 | needs: release 244 | name: Release Integration 245 | if: needs.release.outputs.releases 246 | uses: ./.github/workflows/release-integration.yml 247 | permissions: 248 | id-token: write 249 | secrets: 250 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 251 | with: 252 | releases: ${{ needs.release.outputs.releases }} 253 | 254 | post-release-integration: 255 | needs: [ release, release-integration, post-release ] 256 | name: Post Release Integration - Release 257 | if: github.repository_owner == 'npm' && needs.release.outputs.releases && always() 258 | runs-on: ubuntu-latest 259 | defaults: 260 | run: 261 | shell: bash 262 | steps: 263 | - name: Get Post Release Conclusion 264 | id: conclusion 265 | run: | 266 | if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then 267 | result="x" 268 | elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then 269 | result="heavy_multiplication_x" 270 | else 271 | result="white_check_mark" 272 | fi 273 | echo "result=$result" >> $GITHUB_OUTPUT 274 | - name: Find Release PR Comment 275 | uses: peter-evans/find-comment@v2 276 | id: found-comment 277 | with: 278 | issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }} 279 | comment-author: 'github-actions[bot]' 280 | body-includes: '## Release Workflow' 281 | - name: Create Release PR Comment Text 282 | id: comment-text 283 | if: steps.found-comment.outputs.comment-id 284 | uses: actions/github-script@v7 285 | env: 286 | RESULT: ${{ steps.conclusion.outputs.result }} 287 | BODY: ${{ steps.found-comment.outputs.comment-body }} 288 | with: 289 | result-encoding: string 290 | script: | 291 | const { RESULT, BODY } = process.env 292 | const body = [BODY.replace(/(Workflow run: :)[a-z_]+(:)/, `$1${RESULT}$2`)] 293 | if (RESULT !== 'white_check_mark') { 294 | body.push(':rotating_light::rotating_light::rotating_light:') 295 | body.push([ 296 | '@npm/cli-team: The post-release workflow failed for this release.', 297 | 'Manual steps may need to be taken after examining the workflow output.' 298 | ].join(' ')) 299 | body.push(':rotating_light::rotating_light::rotating_light:') 300 | } 301 | return body.join('\n\n').trim() 302 | - name: Update Release PR Comment 303 | if: steps.comment-text.outputs.result 304 | uses: peter-evans/create-or-update-comment@v3 305 | with: 306 | comment-id: ${{ steps.found-comment.outputs.comment-id }} 307 | body: ${{ steps.comment-text.outputs.result }} 308 | edit-mode: 'replace' 309 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | # ignore everything in the root 4 | /* 5 | 6 | !**/.gitignore 7 | !/.commitlintrc.js 8 | !/.eslint.config.js 9 | !/.eslintrc.js 10 | !/.eslintrc.local.* 11 | !/.git-blame-ignore-revs 12 | !/.github/ 13 | !/.gitignore 14 | !/.npmrc 15 | !/.prettierignore 16 | !/.prettierrc.js 17 | !/.release-please-manifest.json 18 | !/bin/ 19 | !/CHANGELOG* 20 | !/CODE_OF_CONDUCT.md 21 | !/CONTRIBUTING.md 22 | !/docs/ 23 | !/lib/ 24 | !/LICENSE* 25 | !/map.js 26 | !/package.json 27 | !/README* 28 | !/release-please-config.json 29 | !/scripts/ 30 | !/SECURITY.md 31 | !/tap-snapshots/ 32 | !/test/ 33 | !/tsconfig.json 34 | tap-testdir*/ 35 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ; This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | package-lock=false 4 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "8.1.0" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [8.1.0](https://github.com/npm/hosted-git-info/compare/v8.0.2...v8.1.0) (2025-04-14) 4 | ### Features 5 | * [`ef0865c`](https://github.com/npm/hosted-git-info/commit/ef0865cc5c28700f990bf25d919e2520c944cf55) [#288](https://github.com/npm/hosted-git-info/pull/288) add `HostedGitInfo.fromManifest` (#288) (@ljharb) 6 | ### Chores 7 | * [`ac08fe8`](https://github.com/npm/hosted-git-info/commit/ac08fe89153d19d1fecbd1e5ce5014fad833134c) [#296](https://github.com/npm/hosted-git-info/pull/296) bump @npmcli/template-oss from 4.23.6 to 4.24.3 (#296) (@dependabot[bot], @npm-cli-bot) 8 | 9 | ## [8.0.2](https://github.com/npm/hosted-git-info/compare/v8.0.1...v8.0.2) (2024-11-21) 10 | ### Bug Fixes 11 | * [`cc004ba`](https://github.com/npm/hosted-git-info/commit/cc004bae62d17b90c2fc889fcde5afbcac2fc508) [#280](https://github.com/npm/hosted-git-info/pull/280) even better regex for host fragment (#280) (@wraithgar) 12 | 13 | ## [8.0.1](https://github.com/npm/hosted-git-info/compare/v8.0.0...v8.0.1) (2024-11-20) 14 | ### Bug Fixes 15 | * [`e47b7e4`](https://github.com/npm/hosted-git-info/commit/e47b7e476199820446483aefa0525d4726e49450) [#274](https://github.com/npm/hosted-git-info/pull/274) break up greedy host fragment parsing regex (#274) (@wraithgar) 16 | ### Chores 17 | * [`3d55d13`](https://github.com/npm/hosted-git-info/commit/3d55d1316d1b323b1402ad2c642c6d1f37249058) [#277](https://github.com/npm/hosted-git-info/pull/277) fix workflows for new backport branch (#277) (@wraithgar) 18 | * [`b3e455f`](https://github.com/npm/hosted-git-info/commit/b3e455fd7d66c2c967dba0cc624db8ed142bb86f) [#273](https://github.com/npm/hosted-git-info/pull/273) bump @npmcli/template-oss from 4.23.3 to 4.23.4 (#273) (@dependabot[bot], @npm-cli-bot) 19 | 20 | ## [8.0.0](https://github.com/npm/hosted-git-info/compare/v7.0.2...v8.0.0) (2024-09-03) 21 | ### ⚠️ BREAKING CHANGES 22 | * `hosted-git-info` now supports node `^18.17.0 || >=20.5.0` 23 | ### Bug Fixes 24 | * [`967d930`](https://github.com/npm/hosted-git-info/commit/967d930a3a2adb8b0b55c9d8ddfa1eeb9470f3e1) [#268](https://github.com/npm/hosted-git-info/pull/268) align to npm 10 node engine range (@hashtagchris) 25 | ### Chores 26 | * [`20551b0`](https://github.com/npm/hosted-git-info/commit/20551b02dffa5fdb56d9b89b3521c016c4924ace) [#268](https://github.com/npm/hosted-git-info/pull/268) run template-oss-apply (@hashtagchris) 27 | * [`9a3c062`](https://github.com/npm/hosted-git-info/commit/9a3c062a74dba37c6958a00ee22eb9207d45aefc) [#265](https://github.com/npm/hosted-git-info/pull/265) bump @npmcli/eslint-config from 4.0.5 to 5.0.0 (@dependabot[bot]) 28 | * [`8f0fa04`](https://github.com/npm/hosted-git-info/commit/8f0fa04d0fba8d6a2467acc648a2f568f3baa7ed) [#266](https://github.com/npm/hosted-git-info/pull/266) postinstall for dependabot template-oss PR (@hashtagchris) 29 | * [`e0fe523`](https://github.com/npm/hosted-git-info/commit/e0fe523d96dc023b8e6750aa458b0d816673cb49) [#266](https://github.com/npm/hosted-git-info/pull/266) bump @npmcli/template-oss from 4.23.1 to 4.23.3 (@dependabot[bot]) 30 | 31 | ## [7.0.2](https://github.com/npm/hosted-git-info/compare/v7.0.1...v7.0.2) (2024-05-04) 32 | 33 | ### Bug Fixes 34 | 35 | * [`682fa35`](https://github.com/npm/hosted-git-info/commit/682fa356278e342b93361bb61cfb0e598011b61f) [#249](https://github.com/npm/hosted-git-info/pull/249) linting: no-unused-vars (@lukekarrys) 36 | 37 | ### Chores 38 | 39 | * [`f33287c`](https://github.com/npm/hosted-git-info/commit/f33287c39772f714b41c2d32a5cb9e98b0d00c6f) [#249](https://github.com/npm/hosted-git-info/pull/249) bump @npmcli/template-oss to 4.22.0 (@lukekarrys) 40 | * [`7bbdfd8`](https://github.com/npm/hosted-git-info/commit/7bbdfd8a564ddd5952fd245c38193af17e6a8d2c) [#248](https://github.com/npm/hosted-git-info/pull/248) chore: postinstall for dependabot template-oss PR (@lukekarrys) 41 | * [`0d4310e`](https://github.com/npm/hosted-git-info/commit/0d4310e90809efa2c7f5be586709c821d432a551) [#249](https://github.com/npm/hosted-git-info/pull/249) postinstall for dependabot template-oss PR (@lukekarrys) 42 | * [`2efc69b`](https://github.com/npm/hosted-git-info/commit/2efc69beca342455f1113625c66157f3f5c53af4) [#248](https://github.com/npm/hosted-git-info/pull/248) bump @npmcli/template-oss from 4.21.3 to 4.21.4 (@dependabot[bot]) 43 | 44 | ## [7.0.1](https://github.com/npm/hosted-git-info/compare/v7.0.0...v7.0.1) (2023-09-13) 45 | 46 | ### Bug Fixes 47 | 48 | * [`d7bac33`](https://github.com/npm/hosted-git-info/commit/d7bac33726d6a65788d16e3314f52449f0da58c4) [#213](https://github.com/npm/hosted-git-info/pull/213) remove sourcehut bugstemplate (#213) (@vladh) 49 | 50 | ## [7.0.0](https://github.com/npm/hosted-git-info/compare/v6.1.1...v7.0.0) (2023-08-14) 51 | 52 | ### ⚠️ BREAKING CHANGES 53 | 54 | * support for node 14 has been removed 55 | 56 | ### Bug Fixes 57 | 58 | * [`f9f7fde`](https://github.com/npm/hosted-git-info/commit/f9f7fde1385d3f99ed7a52b9d4b079d8074fc99f) [#209](https://github.com/npm/hosted-git-info/pull/209) use lru-cache named export (@lukekarrys) 59 | * [`c98e908`](https://github.com/npm/hosted-git-info/commit/c98e90807775bf5c306a30426d7f6c6ebe9842d5) [#209](https://github.com/npm/hosted-git-info/pull/209) drop node14 support (@lukekarrys) 60 | 61 | ### Dependencies 62 | 63 | * [`ecdd7de`](https://github.com/npm/hosted-git-info/commit/ecdd7decf24f66297ca5f459b4f1f36d41352e23) [#209](https://github.com/npm/hosted-git-info/pull/209) bump lru-cache from 7.18.3 to 10.0.1 64 | 65 | ## [6.1.1](https://github.com/npm/hosted-git-info/compare/v6.1.0...v6.1.1) (2022-10-27) 66 | 67 | ### Bug Fixes 68 | 69 | * [`f03bfbd`](https://github.com/npm/hosted-git-info/commit/f03bfbd3022c8f6283a991ff879ed97704ac35fa) [#176](https://github.com/npm/hosted-git-info/pull/176) only correct protocols when called from githost (@lukekarrys) 70 | 71 | ## [6.1.0](https://github.com/npm/hosted-git-info/compare/v6.0.0...v6.1.0) (2022-10-26) 72 | 73 | ### Features 74 | 75 | * [`a44bd35`](https://github.com/npm/hosted-git-info/commit/a44bd35820eaa6878f13ee12eba5dca6425ea2bd) [#172](https://github.com/npm/hosted-git-info/pull/172) add separate static method for just parsing urls (@lukekarrys) 76 | 77 | ## [6.0.0](https://github.com/npm/hosted-git-info/compare/v5.1.0...v6.0.0) (2022-10-12) 78 | 79 | ### ⚠️ BREAKING CHANGES 80 | 81 | * `GitHost` now has a static `addHost` method to use instead of manually editing the object from `lib/git-host-info.js`. 82 | * set default git ref to HEAD 83 | * `hosted-git-info` is now compatible with the following semver range for node: `^14.17.0 || ^16.13.0 || >=18.0.0` 84 | 85 | ### Features 86 | 87 | * [`9e0ce62`](https://github.com/npm/hosted-git-info/commit/9e0ce62b9aadb2a9cfe8999e96b004a5de4edfdf) [#142](https://github.com/npm/hosted-git-info/pull/142) refactor (@lukekarrys) 88 | * [`89155e8`](https://github.com/npm/hosted-git-info/commit/89155e8799369f20ae71713f64e3d0f664192a58) set default git ref to HEAD (@darcyclarke) 89 | * [`9ed9c38`](https://github.com/npm/hosted-git-info/commit/9ed9c38002f899ad2628f96b27b2ec9fecb4662f) [#162](https://github.com/npm/hosted-git-info/pull/162) postinstall for dependabot template-oss PR (@lukekarrys) 90 | 91 | ### Bug Fixes 92 | 93 | * [`61ca7fb`](https://github.com/npm/hosted-git-info/commit/61ca7fb8f003299693e23f351eea589c38a3602c) [#152](https://github.com/npm/hosted-git-info/pull/152) parse branch names containing @ (@lukekarrys) 94 | * [`3cd4a98`](https://github.com/npm/hosted-git-info/commit/3cd4a9881e20d3a59bf3bb470661a29208824dd6) ignore colons after hash when correcting scp urls (@lukekarrys) 95 | 96 | ## [5.1.0](https://github.com/npm/hosted-git-info/compare/v5.0.0...v5.1.0) (2022-08-09) 97 | 98 | 99 | ### Features 100 | 101 | * add method to get an edit link to a file ([ad02952](https://github.com/npm/hosted-git-info/commit/ad02952f89fbdc99e67ae0d5308029395bde3331)) 102 | 103 | 104 | ### Bug Fixes 105 | 106 | * add comments to empty catch blocks for linting ([70a770d](https://github.com/npm/hosted-git-info/commit/70a770d1202128e15887d69dfd5c930e4ff29a00)) 107 | 108 | ## [5.0.0](https://www.github.com/npm/hosted-git-info/compare/v4.1.0...v5.0.0) (2022-03-14) 109 | 110 | 111 | ### ⚠ BREAKING CHANGES 112 | 113 | * this drops support for node 10 and non-LTS versions of node 12 and node 14 114 | 115 | ### Bug Fixes 116 | 117 | * move files to lib ([a3f4836](https://www.github.com/npm/hosted-git-info/commit/a3f4836ba0a75b355c004e1991e8dd1e6321a983)) 118 | 119 | 120 | * @npmcli/template-oss@2.9.2 ([c42e1f2](https://www.github.com/npm/hosted-git-info/commit/c42e1f216542ead9d0d328704c5db02204f15ce8)) 121 | 122 | 123 | ### Dependencies 124 | 125 | * bump lru-cache from 6.0.0 to 7.5.1 ([#128](https://www.github.com/npm/hosted-git-info/issues/128)) ([5b0b3b5](https://www.github.com/npm/hosted-git-info/commit/5b0b3b50bd36f659037e3b82a7ff47b0eff3b9f9)) 126 | 127 | ## [4.0.0](https://github.com/npm/hosted-git-info/compare/v3.0.7...v4.0.0) (2021-03-09) 128 | 129 | 130 | ### Features 131 | 132 | * rewrite the entire module: all internals have been rewritten to maintain a similar contract but to remove excessive use of regular expressions, unnecessary loops, the custom string templating engine, and various other bits of complexity ([c218b9](https://github.com/npm/hosted-git-info/commit/c218b9ec90cf6a818341cd0f7b03ea65793b185b)) 133 | 134 | 135 | ### BREAKING CHANGES 136 | 137 | * extending with custom providers has changed ([c218b9](https://github.com/npm/hosted-git-info/commit/c218b9ec90cf6a818341cd0f7b03ea65793b185b)) 138 | 139 | 140 | 141 | <a name="3.0.8"></a> 142 | ## [3.0.8](https://github.com/npm/hosted-git-info/compare/v3.0.7...v3.0.8) (2021-01-28) 143 | 144 | 145 | ### Bug Fixes 146 | 147 | * simplify the regular expression for shortcut matching ([bede0dc](https://github.com/npm/hosted-git-info/commit/bede0dc)), closes [#76](https://github.com/npm/hosted-git-info/issues/76) 148 | 149 | 150 | 151 | <a name="3.0.7"></a> 152 | ## [3.0.7](https://github.com/npm/hosted-git-info/compare/v3.0.6...v3.0.7) (2020-10-15) 153 | 154 | 155 | ### Bug Fixes 156 | 157 | * correctly filter out urls for tarballs in gitlab ([eb5bd5a](https://github.com/npm/hosted-git-info/commit/eb5bd5a)), closes [#69](https://github.com/npm/hosted-git-info/issues/69) 158 | 159 | 160 | 161 | <a name="3.0.6"></a> 162 | ## [3.0.6](https://github.com/npm/hosted-git-info/compare/v3.0.5...v3.0.6) (2020-10-12) 163 | 164 | 165 | ### Bug Fixes 166 | 167 | * support to github gist legacy hash length ([c067102](https://github.com/npm/hosted-git-info/commit/c067102)), closes [#68](https://github.com/npm/hosted-git-info/issues/68) 168 | 169 | 170 | 171 | <a name="3.0.5"></a> 172 | ## [3.0.5](https://github.com/npm/hosted-git-info/compare/v3.0.4...v3.0.5) (2020-07-11) 173 | 174 | 175 | 176 | <a name="3.0.4"></a> 177 | ## [3.0.4](https://github.com/npm/hosted-git-info/compare/v3.0.3...v3.0.4) (2020-02-26) 178 | 179 | 180 | ### Bug Fixes 181 | 182 | * Do not pass scp-style URLs to the WhatWG url.URL ([0835306](https://github.com/npm/hosted-git-info/commit/0835306)), closes [#60](https://github.com/npm/hosted-git-info/issues/60) [#63](https://github.com/npm/hosted-git-info/issues/63) 183 | 184 | 185 | 186 | <a name="3.0.3"></a> 187 | ## [3.0.3](https://github.com/npm/hosted-git-info/compare/v3.0.2...v3.0.3) (2020-02-25) 188 | 189 | 190 | 191 | <a name="3.0.2"></a> 192 | ## [3.0.2](https://github.com/npm/hosted-git-info/compare/v3.0.1...v3.0.2) (2019-10-08) 193 | 194 | 195 | ### Bug Fixes 196 | 197 | * do not encodeURIComponent the domain ([3e5fbec](https://github.com/npm/hosted-git-info/commit/3e5fbec)), closes [#53](https://github.com/npm/hosted-git-info/issues/53) 198 | 199 | 200 | 201 | <a name="3.0.1"></a> 202 | ## [3.0.1](https://github.com/npm/hosted-git-info/compare/v3.0.0...v3.0.1) (2019-10-07) 203 | 204 | 205 | ### Bug Fixes 206 | 207 | * update pathmatch for gitlab ([e3e3054](https://github.com/npm/hosted-git-info/commit/e3e3054)), closes [#52](https://github.com/npm/hosted-git-info/issues/52) 208 | * updated pathmatch for gitlab ([fa87af7](https://github.com/npm/hosted-git-info/commit/fa87af7)) 209 | 210 | 211 | 212 | <a name="3.0.0"></a> 213 | # [3.0.0](https://github.com/npm/hosted-git-info/compare/v2.8.3...v3.0.0) (2019-08-12) 214 | 215 | 216 | ### Bug Fixes 217 | 218 | * **cache:** Switch to lru-cache to save ourselves from unlimited memory consumption ([37c2891](https://github.com/npm/hosted-git-info/commit/37c2891)), closes [#38](https://github.com/npm/hosted-git-info/issues/38) 219 | 220 | 221 | ### BREAKING CHANGES 222 | 223 | * **cache:** Drop support for node 0.x 224 | 225 | 226 | 227 | <a name="2.8.3"></a> 228 | ## [2.8.3](https://github.com/npm/hosted-git-info/compare/v2.8.2...v2.8.3) (2019-08-12) 229 | 230 | 231 | 232 | <a name="2.8.2"></a> 233 | ## [2.8.2](https://github.com/npm/hosted-git-info/compare/v2.8.1...v2.8.2) (2019-08-05) 234 | 235 | 236 | ### Bug Fixes 237 | 238 | * http protocol use sshurl by default ([3b1d629](https://github.com/npm/hosted-git-info/commit/3b1d629)), closes [#48](https://github.com/npm/hosted-git-info/issues/48) 239 | 240 | 241 | 242 | <a name="2.8.1"></a> 243 | ## [2.8.1](https://github.com/npm/hosted-git-info/compare/v2.8.0...v2.8.1) (2019-08-05) 244 | 245 | 246 | ### Bug Fixes 247 | 248 | * ignore noCommittish on tarball url generation ([5d4a8d7](https://github.com/npm/hosted-git-info/commit/5d4a8d7)) 249 | * use gist tarball url that works for anonymous gists ([1692435](https://github.com/npm/hosted-git-info/commit/1692435)) 250 | 251 | 252 | 253 | <a name="2.8.0"></a> 254 | # [2.8.0](https://github.com/npm/hosted-git-info/compare/v2.7.1...v2.8.0) (2019-08-05) 255 | 256 | 257 | ### Bug Fixes 258 | 259 | * Allow slashes in gitlab project section ([bbcf7b2](https://github.com/npm/hosted-git-info/commit/bbcf7b2)), closes [#46](https://github.com/npm/hosted-git-info/issues/46) [#43](https://github.com/npm/hosted-git-info/issues/43) 260 | * **git-host:** disallow URI-encoded slash (%2F) in `path` ([3776fa5](https://github.com/npm/hosted-git-info/commit/3776fa5)), closes [#44](https://github.com/npm/hosted-git-info/issues/44) 261 | * **gitlab:** Do not URL encode slashes in project name for GitLab https URL ([cbf04f9](https://github.com/npm/hosted-git-info/commit/cbf04f9)), closes [#47](https://github.com/npm/hosted-git-info/issues/47) 262 | * do not allow invalid gist urls ([d5cf830](https://github.com/npm/hosted-git-info/commit/d5cf830)) 263 | * **cache:** Switch to lru-cache to save ourselves from unlimited memory consumption ([e518222](https://github.com/npm/hosted-git-info/commit/e518222)), closes [#38](https://github.com/npm/hosted-git-info/issues/38) 264 | 265 | 266 | ### Features 267 | 268 | * give these objects a name ([60abaea](https://github.com/npm/hosted-git-info/commit/60abaea)) 269 | 270 | 271 | 272 | <a name="2.7.1"></a> 273 | ## [2.7.1](https://github.com/npm/hosted-git-info/compare/v2.7.0...v2.7.1) (2018-07-07) 274 | 275 | 276 | ### Bug Fixes 277 | 278 | * **index:** Guard against non-string types ([5bc580d](https://github.com/npm/hosted-git-info/commit/5bc580d)) 279 | * **parse:** Crash on strings that parse to having no host ([c931482](https://github.com/npm/hosted-git-info/commit/c931482)), closes [#35](https://github.com/npm/hosted-git-info/issues/35) 280 | 281 | 282 | 283 | <a name="2.7.0"></a> 284 | # [2.7.0](https://github.com/npm/hosted-git-info/compare/v2.6.1...v2.7.0) (2018-07-06) 285 | 286 | 287 | ### Bug Fixes 288 | 289 | * **github tarball:** update github tarballtemplate ([6efd582](https://github.com/npm/hosted-git-info/commit/6efd582)), closes [#34](https://github.com/npm/hosted-git-info/issues/34) 290 | * **gitlab docs:** switched to lowercase anchors for readmes ([701bcd1](https://github.com/npm/hosted-git-info/commit/701bcd1)) 291 | 292 | 293 | ### Features 294 | 295 | * **all:** Support www. prefixes on hostnames ([3349575](https://github.com/npm/hosted-git-info/commit/3349575)), closes [#32](https://github.com/npm/hosted-git-info/issues/32) 296 | 297 | 298 | 299 | <a name="2.6.1"></a> 300 | ## [2.6.1](https://github.com/npm/hosted-git-info/compare/v2.6.0...v2.6.1) (2018-06-25) 301 | 302 | ### Bug Fixes 303 | 304 | * **Revert:** "compat: remove Object.assign fallback ([#25](https://github.com/npm/hosted-git-info/issues/25))" ([cce5a62](https://github.com/npm/hosted-git-info/commit/cce5a62)) 305 | * **Revert:** "git-host: fix forgotten extend()" ([a815ec9](https://github.com/npm/hosted-git-info/commit/a815ec9)) 306 | 307 | 308 | 309 | <a name="2.6.0"></a> 310 | # [2.6.0](https://github.com/npm/hosted-git-info/compare/v2.5.0...v2.6.0) (2018-03-07) 311 | 312 | 313 | ### Bug Fixes 314 | 315 | * **compat:** remove Object.assign fallback ([#25](https://github.com/npm/hosted-git-info/issues/25)) ([627ab55](https://github.com/npm/hosted-git-info/commit/627ab55)) 316 | * **git-host:** fix forgotten extend() ([eba1f7b](https://github.com/npm/hosted-git-info/commit/eba1f7b)) 317 | 318 | 319 | ### Features 320 | 321 | * **browse:** fragment support for browse() ([#28](https://github.com/npm/hosted-git-info/issues/28)) ([cd5e5bb](https://github.com/npm/hosted-git-info/commit/cd5e5bb)) 322 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | All interactions in this repo are covered by the [npm Code of 4 | Conduct](https://docs.npmjs.com/policies/conduct) 5 | 6 | The npm cli team may, at its own discretion, moderate, remove, or edit 7 | any interactions such as pull requests, issues, and comments. 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | # Contributing 4 | 5 | ## Code of Conduct 6 | 7 | All interactions in the **npm** organization on GitHub are considered to be covered by our standard [Code of Conduct](https://docs.npmjs.com/policies/conduct). 8 | 9 | ## Reporting Bugs 10 | 11 | Before submitting a new bug report please search for an existing or similar report. 12 | 13 | Use one of our existing issue templates if you believe you've come across a unique problem. 14 | 15 | Duplicate issues, or issues that don't use one of our templates may get closed without a response. 16 | 17 | ## Pull Request Conventions 18 | 19 | ### Commits 20 | 21 | We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 22 | 23 | When opening a pull request please be sure that either the pull request title, or each commit in the pull request, has one of the following prefixes: 24 | 25 | - `feat`: For when introducing a new feature. The result will be a new semver minor version of the package when it is next published. 26 | - `fix`: For bug fixes. The result will be a new semver patch version of the package when it is next published. 27 | - `docs`: For documentation updates. The result will be a new semver patch version of the package when it is next published. 28 | - `chore`: For changes that do not affect the published module. Often these are changes to tests. The result will be *no* change to the version of the package when it is next published (as the commit does not affect the published version). 29 | 30 | ### Test Coverage 31 | 32 | Pull requests made against this repo will run `npm test` automatically. Please make sure tests pass locally before submitting a PR. 33 | 34 | Every new feature or bug fix should come with a corresponding test or tests that validate the solutions. Testing also reports on code coverage and will fail if code coverage drops. 35 | 36 | ### Linting 37 | 38 | Linting is also done automatically once tests pass. `npm run lintfix` will fix most linting errors automatically. 39 | 40 | Please make sure linting passes before submitting a PR. 41 | 42 | ## What _not_ to contribute? 43 | 44 | ### Dependencies 45 | 46 | It should be noted that our team does not accept third-party dependency updates/PRs. If you submit a PR trying to update our dependencies we will close it with or without a reference to these contribution guidelines. 47 | 48 | ### Tools/Automation 49 | 50 | Our core team is responsible for the maintenance of the tooling/automation in this project and we ask contributors to not make changes to these when contributing (e.g. `.github/*`, `.eslintrc.json`, `.licensee.json`). Most of those files also have a header at the top to remind folks they are automatically generated. Pull requests that alter these will not be accepted. 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Rebecca Turner 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hosted-git-info 2 | 3 | This will let you identify and transform various git hosts URLs between 4 | protocols. It also can tell you what the URL is for the raw path for 5 | particular file for direct access without git. 6 | 7 | ## Example 8 | 9 | ```javascript 10 | const hostedGitInfo = require("hosted-git-info") 11 | const info = hostedGitInfo.fromUrl("git@github.com:npm/hosted-git-info.git", opts) 12 | /* info looks like: 13 | { 14 | type: "github", 15 | domain: "github.com", 16 | user: "npm", 17 | project: "hosted-git-info" 18 | } 19 | */ 20 | ``` 21 | 22 | If the URL can't be matched with a git host, `null` will be returned. We 23 | can match git, ssh and https urls. Additionally, we can match ssh connect 24 | strings (`git@github.com:npm/hosted-git-info`) and shortcuts (eg, 25 | `github:npm/hosted-git-info`). GitHub specifically, is detected in the case 26 | of a third, unprefixed, form: `npm/hosted-git-info`. 27 | 28 | If it does match, the returned object has properties of: 29 | 30 | * info.type -- The short name of the service 31 | * info.domain -- The domain for git protocol use 32 | * info.user -- The name of the user/org on the git host 33 | * info.project -- The name of the project on the git host 34 | 35 | ## Version Contract 36 | 37 | The major version will be bumped any time… 38 | 39 | * The constructor stops accepting URLs that it previously accepted. 40 | * A method is removed. 41 | * A method can no longer accept the number and type of arguments it previously accepted. 42 | * A method can return a different type than it currently returns. 43 | 44 | Implications: 45 | 46 | * I do not consider the specific format of the urls returned from, say 47 | `.https()` to be a part of the contract. The contract is that it will 48 | return a string that can be used to fetch the repo via HTTPS. But what 49 | that string looks like, specifically, can change. 50 | * Dropping support for a hosted git provider would constitute a breaking 51 | change. 52 | 53 | ## Usage 54 | 55 | ### const info = hostedGitInfo.fromUrl(gitSpecifier[, options]) 56 | 57 | * *gitSpecifer* is a URL of a git repository or a SCP-style specifier of one. 58 | * *options* is an optional object. It can have the following properties: 59 | * *noCommittish* — If true then committishes won't be included in generated URLs. 60 | * *noGitPlus* — If true then `git+` won't be prefixed on URLs. 61 | 62 | ### const infoOrURL = hostedGitInfo.fromManifest(manifest[, options]) 63 | 64 | * *manifest* is a package manifest, such as that returned by [`pacote.manifest()`](https://npmjs.com/pacote) 65 | * *options* is an optional object. It can have the same properties as `fromUrl` above. 66 | 67 | ## Methods 68 | 69 | All of the methods take the same options as the `fromUrl` factory. Options 70 | provided to a method override those provided to the constructor. 71 | 72 | * info.file(path, opts) 73 | 74 | Given the path of a file relative to the repository, returns a URL for 75 | directly fetching it from the githost. If no committish was set then 76 | `HEAD` will be used as the default. 77 | 78 | For example `hostedGitInfo.fromUrl("git@github.com:npm/hosted-git-info.git#v1.0.0").file("package.json")` 79 | would return `https://raw.githubusercontent.com/npm/hosted-git-info/v1.0.0/package.json` 80 | 81 | * info.shortcut(opts) 82 | 83 | eg, `github:npm/hosted-git-info` 84 | 85 | * info.browse(path, fragment, opts) 86 | 87 | eg, `https://github.com/npm/hosted-git-info/tree/v1.2.0`, 88 | `https://github.com/npm/hosted-git-info/tree/v1.2.0/package.json`, 89 | `https://github.com/npm/hosted-git-info/tree/v1.2.0/README.md#supported-hosts` 90 | 91 | * info.bugs(opts) 92 | 93 | eg, `https://github.com/npm/hosted-git-info/issues` 94 | 95 | * info.docs(opts) 96 | 97 | eg, `https://github.com/npm/hosted-git-info/tree/v1.2.0#readme` 98 | 99 | * info.https(opts) 100 | 101 | eg, `git+https://github.com/npm/hosted-git-info.git` 102 | 103 | * info.sshurl(opts) 104 | 105 | eg, `git+ssh://git@github.com/npm/hosted-git-info.git` 106 | 107 | * info.ssh(opts) 108 | 109 | eg, `git@github.com:npm/hosted-git-info.git` 110 | 111 | * info.path(opts) 112 | 113 | eg, `npm/hosted-git-info` 114 | 115 | * info.tarball(opts) 116 | 117 | eg, `https://github.com/npm/hosted-git-info/archive/v1.2.0.tar.gz` 118 | 119 | * info.getDefaultRepresentation() 120 | 121 | Returns the default output type. The default output type is based on the 122 | string you passed in to be parsed 123 | 124 | * info.toString(opts) 125 | 126 | Uses the getDefaultRepresentation to call one of the other methods to get a URL for 127 | this resource. As such `hostedGitInfo.fromUrl(url).toString()` will give 128 | you a normalized version of the URL that still uses the same protocol. 129 | 130 | Shortcuts will still be returned as shortcuts, but the special case github 131 | form of `org/project` will be normalized to `github:org/project`. 132 | 133 | SSH connect strings will be normalized into `git+ssh` URLs. 134 | 135 | ## Supported hosts 136 | 137 | Currently this supports GitHub (including Gists), Bitbucket, GitLab and Sourcehut. 138 | Pull requests for additional hosts welcome. 139 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | GitHub takes the security of our software products and services seriously, including the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). 4 | 5 | If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways. 6 | 7 | If the vulnerability you have found is *not* [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) or if you do not wish to be considered for a bounty reward, please report the issue to us directly through [opensource-security@github.com](mailto:opensource-security@github.com). 8 | 9 | If the vulnerability you have found is [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) and you would like for your finding to be considered for a bounty reward, please submit the vulnerability to us through [HackerOne](https://hackerone.com/github) in order to be eligible to receive a bounty award. 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 12 | 13 | Thanks for helping make GitHub safe for everyone. 14 | -------------------------------------------------------------------------------- /lib/from-url.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const parseUrl = require('./parse-url') 4 | 5 | // look for github shorthand inputs, such as npm/cli 6 | const isGitHubShorthand = (arg) => { 7 | // it cannot contain whitespace before the first # 8 | // it cannot start with a / because that's probably an absolute file path 9 | // but it must include a slash since repos are username/repository 10 | // it cannot start with a . because that's probably a relative file path 11 | // it cannot start with an @ because that's a scoped package if it passes the other tests 12 | // it cannot contain a : before a # because that tells us that there's a protocol 13 | // a second / may not exist before a # 14 | const firstHash = arg.indexOf('#') 15 | const firstSlash = arg.indexOf('/') 16 | const secondSlash = arg.indexOf('/', firstSlash + 1) 17 | const firstColon = arg.indexOf(':') 18 | const firstSpace = /\s/.exec(arg) 19 | const firstAt = arg.indexOf('@') 20 | 21 | const spaceOnlyAfterHash = !firstSpace || (firstHash > -1 && firstSpace.index > firstHash) 22 | const atOnlyAfterHash = firstAt === -1 || (firstHash > -1 && firstAt > firstHash) 23 | const colonOnlyAfterHash = firstColon === -1 || (firstHash > -1 && firstColon > firstHash) 24 | const secondSlashOnlyAfterHash = secondSlash === -1 || (firstHash > -1 && secondSlash > firstHash) 25 | const hasSlash = firstSlash > 0 26 | // if a # is found, what we really want to know is that the character 27 | // immediately before # is not a / 28 | const doesNotEndWithSlash = firstHash > -1 ? arg[firstHash - 1] !== '/' : !arg.endsWith('/') 29 | const doesNotStartWithDot = !arg.startsWith('.') 30 | 31 | return spaceOnlyAfterHash && hasSlash && doesNotEndWithSlash && 32 | doesNotStartWithDot && atOnlyAfterHash && colonOnlyAfterHash && 33 | secondSlashOnlyAfterHash 34 | } 35 | 36 | module.exports = (giturl, opts, { gitHosts, protocols }) => { 37 | if (!giturl) { 38 | return 39 | } 40 | 41 | const correctedUrl = isGitHubShorthand(giturl) ? `github:${giturl}` : giturl 42 | const parsed = parseUrl(correctedUrl, protocols) 43 | if (!parsed) { 44 | return 45 | } 46 | 47 | const gitHostShortcut = gitHosts.byShortcut[parsed.protocol] 48 | const gitHostDomain = gitHosts.byDomain[parsed.hostname.startsWith('www.') 49 | ? parsed.hostname.slice(4) 50 | : parsed.hostname] 51 | const gitHostName = gitHostShortcut || gitHostDomain 52 | if (!gitHostName) { 53 | return 54 | } 55 | 56 | const gitHostInfo = gitHosts[gitHostShortcut || gitHostDomain] 57 | let auth = null 58 | if (protocols[parsed.protocol]?.auth && (parsed.username || parsed.password)) { 59 | auth = `${parsed.username}${parsed.password ? ':' + parsed.password : ''}` 60 | } 61 | 62 | let committish = null 63 | let user = null 64 | let project = null 65 | let defaultRepresentation = null 66 | 67 | try { 68 | if (gitHostShortcut) { 69 | let pathname = parsed.pathname.startsWith('/') ? parsed.pathname.slice(1) : parsed.pathname 70 | const firstAt = pathname.indexOf('@') 71 | // we ignore auth for shortcuts, so just trim it out 72 | if (firstAt > -1) { 73 | pathname = pathname.slice(firstAt + 1) 74 | } 75 | 76 | const lastSlash = pathname.lastIndexOf('/') 77 | if (lastSlash > -1) { 78 | user = decodeURIComponent(pathname.slice(0, lastSlash)) 79 | // we want nulls only, never empty strings 80 | if (!user) { 81 | user = null 82 | } 83 | project = decodeURIComponent(pathname.slice(lastSlash + 1)) 84 | } else { 85 | project = decodeURIComponent(pathname) 86 | } 87 | 88 | if (project.endsWith('.git')) { 89 | project = project.slice(0, -4) 90 | } 91 | 92 | if (parsed.hash) { 93 | committish = decodeURIComponent(parsed.hash.slice(1)) 94 | } 95 | 96 | defaultRepresentation = 'shortcut' 97 | } else { 98 | if (!gitHostInfo.protocols.includes(parsed.protocol)) { 99 | return 100 | } 101 | 102 | const segments = gitHostInfo.extract(parsed) 103 | if (!segments) { 104 | return 105 | } 106 | 107 | user = segments.user && decodeURIComponent(segments.user) 108 | project = decodeURIComponent(segments.project) 109 | committish = decodeURIComponent(segments.committish) 110 | defaultRepresentation = protocols[parsed.protocol]?.name || parsed.protocol.slice(0, -1) 111 | } 112 | } catch (err) { 113 | /* istanbul ignore else */ 114 | if (err instanceof URIError) { 115 | return 116 | } else { 117 | throw err 118 | } 119 | } 120 | 121 | return [gitHostName, user, auth, project, committish, defaultRepresentation, opts] 122 | } 123 | -------------------------------------------------------------------------------- /lib/hosts.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | 3 | 'use strict' 4 | 5 | const maybeJoin = (...args) => args.every(arg => arg) ? args.join('') : '' 6 | const maybeEncode = (arg) => arg ? encodeURIComponent(arg) : '' 7 | const formatHashFragment = (f) => f.toLowerCase() 8 | .replace(/^\W+/g, '') // strip leading non-characters 9 | .replace(/(?<!\W)\W+$/, '') // strip trailing non-characters 10 | .replace(/\//g, '') // strip all slashes 11 | .replace(/\W+/g, '-') // replace remaining non-characters with '-' 12 | 13 | const defaults = { 14 | sshtemplate: ({ domain, user, project, committish }) => 15 | `git@${domain}:${user}/${project}.git${maybeJoin('#', committish)}`, 16 | sshurltemplate: ({ domain, user, project, committish }) => 17 | `git+ssh://git@${domain}/${user}/${project}.git${maybeJoin('#', committish)}`, 18 | edittemplate: ({ domain, user, project, committish, editpath, path }) => 19 | `https://${domain}/${user}/${project}${maybeJoin('/', editpath, '/', maybeEncode(committish || 'HEAD'), '/', path)}`, 20 | browsetemplate: ({ domain, user, project, committish, treepath }) => 21 | `https://${domain}/${user}/${project}${maybeJoin('/', treepath, '/', maybeEncode(committish))}`, 22 | browsetreetemplate: ({ domain, user, project, committish, treepath, path, fragment, hashformat }) => 23 | `https://${domain}/${user}/${project}/${treepath}/${maybeEncode(committish || 'HEAD')}/${path}${maybeJoin('#', hashformat(fragment || ''))}`, 24 | browseblobtemplate: ({ domain, user, project, committish, blobpath, path, fragment, hashformat }) => 25 | `https://${domain}/${user}/${project}/${blobpath}/${maybeEncode(committish || 'HEAD')}/${path}${maybeJoin('#', hashformat(fragment || ''))}`, 26 | docstemplate: ({ domain, user, project, treepath, committish }) => 27 | `https://${domain}/${user}/${project}${maybeJoin('/', treepath, '/', maybeEncode(committish))}#readme`, 28 | httpstemplate: ({ auth, domain, user, project, committish }) => 29 | `git+https://${maybeJoin(auth, '@')}${domain}/${user}/${project}.git${maybeJoin('#', committish)}`, 30 | filetemplate: ({ domain, user, project, committish, path }) => 31 | `https://${domain}/${user}/${project}/raw/${maybeEncode(committish || 'HEAD')}/${path}`, 32 | shortcuttemplate: ({ type, user, project, committish }) => 33 | `${type}:${user}/${project}${maybeJoin('#', committish)}`, 34 | pathtemplate: ({ user, project, committish }) => 35 | `${user}/${project}${maybeJoin('#', committish)}`, 36 | bugstemplate: ({ domain, user, project }) => 37 | `https://${domain}/${user}/${project}/issues`, 38 | hashformat: formatHashFragment, 39 | } 40 | 41 | const hosts = {} 42 | hosts.github = { 43 | // First two are insecure and generally shouldn't be used any more, but 44 | // they are still supported. 45 | protocols: ['git:', 'http:', 'git+ssh:', 'git+https:', 'ssh:', 'https:'], 46 | domain: 'github.com', 47 | treepath: 'tree', 48 | blobpath: 'blob', 49 | editpath: 'edit', 50 | filetemplate: ({ auth, user, project, committish, path }) => 51 | `https://${maybeJoin(auth, '@')}raw.githubusercontent.com/${user}/${project}/${maybeEncode(committish || 'HEAD')}/${path}`, 52 | gittemplate: ({ auth, domain, user, project, committish }) => 53 | `git://${maybeJoin(auth, '@')}${domain}/${user}/${project}.git${maybeJoin('#', committish)}`, 54 | tarballtemplate: ({ domain, user, project, committish }) => 55 | `https://codeload.${domain}/${user}/${project}/tar.gz/${maybeEncode(committish || 'HEAD')}`, 56 | extract: (url) => { 57 | let [, user, project, type, committish] = url.pathname.split('/', 5) 58 | if (type && type !== 'tree') { 59 | return 60 | } 61 | 62 | if (!type) { 63 | committish = url.hash.slice(1) 64 | } 65 | 66 | if (project && project.endsWith('.git')) { 67 | project = project.slice(0, -4) 68 | } 69 | 70 | if (!user || !project) { 71 | return 72 | } 73 | 74 | return { user, project, committish } 75 | }, 76 | } 77 | 78 | hosts.bitbucket = { 79 | protocols: ['git+ssh:', 'git+https:', 'ssh:', 'https:'], 80 | domain: 'bitbucket.org', 81 | treepath: 'src', 82 | blobpath: 'src', 83 | editpath: '?mode=edit', 84 | edittemplate: ({ domain, user, project, committish, treepath, path, editpath }) => 85 | `https://${domain}/${user}/${project}${maybeJoin('/', treepath, '/', maybeEncode(committish || 'HEAD'), '/', path, editpath)}`, 86 | tarballtemplate: ({ domain, user, project, committish }) => 87 | `https://${domain}/${user}/${project}/get/${maybeEncode(committish || 'HEAD')}.tar.gz`, 88 | extract: (url) => { 89 | let [, user, project, aux] = url.pathname.split('/', 4) 90 | if (['get'].includes(aux)) { 91 | return 92 | } 93 | 94 | if (project && project.endsWith('.git')) { 95 | project = project.slice(0, -4) 96 | } 97 | 98 | if (!user || !project) { 99 | return 100 | } 101 | 102 | return { user, project, committish: url.hash.slice(1) } 103 | }, 104 | } 105 | 106 | hosts.gitlab = { 107 | protocols: ['git+ssh:', 'git+https:', 'ssh:', 'https:'], 108 | domain: 'gitlab.com', 109 | treepath: 'tree', 110 | blobpath: 'tree', 111 | editpath: '-/edit', 112 | httpstemplate: ({ auth, domain, user, project, committish }) => 113 | `git+https://${maybeJoin(auth, '@')}${domain}/${user}/${project}.git${maybeJoin('#', committish)}`, 114 | tarballtemplate: ({ domain, user, project, committish }) => 115 | `https://${domain}/${user}/${project}/repository/archive.tar.gz?ref=${maybeEncode(committish || 'HEAD')}`, 116 | extract: (url) => { 117 | const path = url.pathname.slice(1) 118 | if (path.includes('/-/') || path.includes('/archive.tar.gz')) { 119 | return 120 | } 121 | 122 | const segments = path.split('/') 123 | let project = segments.pop() 124 | if (project.endsWith('.git')) { 125 | project = project.slice(0, -4) 126 | } 127 | 128 | const user = segments.join('/') 129 | if (!user || !project) { 130 | return 131 | } 132 | 133 | return { user, project, committish: url.hash.slice(1) } 134 | }, 135 | } 136 | 137 | hosts.gist = { 138 | protocols: ['git:', 'git+ssh:', 'git+https:', 'ssh:', 'https:'], 139 | domain: 'gist.github.com', 140 | editpath: 'edit', 141 | sshtemplate: ({ domain, project, committish }) => 142 | `git@${domain}:${project}.git${maybeJoin('#', committish)}`, 143 | sshurltemplate: ({ domain, project, committish }) => 144 | `git+ssh://git@${domain}/${project}.git${maybeJoin('#', committish)}`, 145 | edittemplate: ({ domain, user, project, committish, editpath }) => 146 | `https://${domain}/${user}/${project}${maybeJoin('/', maybeEncode(committish))}/${editpath}`, 147 | browsetemplate: ({ domain, project, committish }) => 148 | `https://${domain}/${project}${maybeJoin('/', maybeEncode(committish))}`, 149 | browsetreetemplate: ({ domain, project, committish, path, hashformat }) => 150 | `https://${domain}/${project}${maybeJoin('/', maybeEncode(committish))}${maybeJoin('#', hashformat(path))}`, 151 | browseblobtemplate: ({ domain, project, committish, path, hashformat }) => 152 | `https://${domain}/${project}${maybeJoin('/', maybeEncode(committish))}${maybeJoin('#', hashformat(path))}`, 153 | docstemplate: ({ domain, project, committish }) => 154 | `https://${domain}/${project}${maybeJoin('/', maybeEncode(committish))}`, 155 | httpstemplate: ({ domain, project, committish }) => 156 | `git+https://${domain}/${project}.git${maybeJoin('#', committish)}`, 157 | filetemplate: ({ user, project, committish, path }) => 158 | `https://gist.githubusercontent.com/${user}/${project}/raw${maybeJoin('/', maybeEncode(committish))}/${path}`, 159 | shortcuttemplate: ({ type, project, committish }) => 160 | `${type}:${project}${maybeJoin('#', committish)}`, 161 | pathtemplate: ({ project, committish }) => 162 | `${project}${maybeJoin('#', committish)}`, 163 | bugstemplate: ({ domain, project }) => 164 | `https://${domain}/${project}`, 165 | gittemplate: ({ domain, project, committish }) => 166 | `git://${domain}/${project}.git${maybeJoin('#', committish)}`, 167 | tarballtemplate: ({ project, committish }) => 168 | `https://codeload.github.com/gist/${project}/tar.gz/${maybeEncode(committish || 'HEAD')}`, 169 | extract: (url) => { 170 | let [, user, project, aux] = url.pathname.split('/', 4) 171 | if (aux === 'raw') { 172 | return 173 | } 174 | 175 | if (!project) { 176 | if (!user) { 177 | return 178 | } 179 | 180 | project = user 181 | user = null 182 | } 183 | 184 | if (project.endsWith('.git')) { 185 | project = project.slice(0, -4) 186 | } 187 | 188 | return { user, project, committish: url.hash.slice(1) } 189 | }, 190 | hashformat: function (fragment) { 191 | return fragment && 'file-' + formatHashFragment(fragment) 192 | }, 193 | } 194 | 195 | hosts.sourcehut = { 196 | protocols: ['git+ssh:', 'https:'], 197 | domain: 'git.sr.ht', 198 | treepath: 'tree', 199 | blobpath: 'tree', 200 | filetemplate: ({ domain, user, project, committish, path }) => 201 | `https://${domain}/${user}/${project}/blob/${maybeEncode(committish) || 'HEAD'}/${path}`, 202 | httpstemplate: ({ domain, user, project, committish }) => 203 | `https://${domain}/${user}/${project}.git${maybeJoin('#', committish)}`, 204 | tarballtemplate: ({ domain, user, project, committish }) => 205 | `https://${domain}/${user}/${project}/archive/${maybeEncode(committish) || 'HEAD'}.tar.gz`, 206 | bugstemplate: () => null, 207 | extract: (url) => { 208 | let [, user, project, aux] = url.pathname.split('/', 4) 209 | 210 | // tarball url 211 | if (['archive'].includes(aux)) { 212 | return 213 | } 214 | 215 | if (project && project.endsWith('.git')) { 216 | project = project.slice(0, -4) 217 | } 218 | 219 | if (!user || !project) { 220 | return 221 | } 222 | 223 | return { user, project, committish: url.hash.slice(1) } 224 | }, 225 | } 226 | 227 | for (const [name, host] of Object.entries(hosts)) { 228 | hosts[name] = Object.assign({}, defaults, host) 229 | } 230 | 231 | module.exports = hosts 232 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { LRUCache } = require('lru-cache') 4 | const hosts = require('./hosts.js') 5 | const fromUrl = require('./from-url.js') 6 | const parseUrl = require('./parse-url.js') 7 | 8 | const cache = new LRUCache({ max: 1000 }) 9 | 10 | function unknownHostedUrl (url) { 11 | try { 12 | const { 13 | protocol, 14 | hostname, 15 | pathname, 16 | } = new URL(url) 17 | 18 | if (!hostname) { 19 | return null 20 | } 21 | 22 | const proto = /(?:git\+)http:$/.test(protocol) ? 'http:' : 'https:' 23 | const path = pathname.replace(/\.git$/, '') 24 | return `${proto}//${hostname}${path}` 25 | } catch { 26 | return null 27 | } 28 | } 29 | 30 | class GitHost { 31 | constructor (type, user, auth, project, committish, defaultRepresentation, opts = {}) { 32 | Object.assign(this, GitHost.#gitHosts[type], { 33 | type, 34 | user, 35 | auth, 36 | project, 37 | committish, 38 | default: defaultRepresentation, 39 | opts, 40 | }) 41 | } 42 | 43 | static #gitHosts = { byShortcut: {}, byDomain: {} } 44 | static #protocols = { 45 | 'git+ssh:': { name: 'sshurl' }, 46 | 'ssh:': { name: 'sshurl' }, 47 | 'git+https:': { name: 'https', auth: true }, 48 | 'git:': { auth: true }, 49 | 'http:': { auth: true }, 50 | 'https:': { auth: true }, 51 | 'git+http:': { auth: true }, 52 | } 53 | 54 | static addHost (name, host) { 55 | GitHost.#gitHosts[name] = host 56 | GitHost.#gitHosts.byDomain[host.domain] = name 57 | GitHost.#gitHosts.byShortcut[`${name}:`] = name 58 | GitHost.#protocols[`${name}:`] = { name } 59 | } 60 | 61 | static fromUrl (giturl, opts) { 62 | if (typeof giturl !== 'string') { 63 | return 64 | } 65 | 66 | const key = giturl + JSON.stringify(opts || {}) 67 | 68 | if (!cache.has(key)) { 69 | const hostArgs = fromUrl(giturl, opts, { 70 | gitHosts: GitHost.#gitHosts, 71 | protocols: GitHost.#protocols, 72 | }) 73 | cache.set(key, hostArgs ? new GitHost(...hostArgs) : undefined) 74 | } 75 | 76 | return cache.get(key) 77 | } 78 | 79 | static fromManifest (manifest, opts = {}) { 80 | if (!manifest || typeof manifest !== 'object') { 81 | return 82 | } 83 | 84 | const r = manifest.repository 85 | // TODO: look into also checking the `bugs`/`homepage` URLs 86 | 87 | const rurl = r && ( 88 | typeof r === 'string' 89 | ? r 90 | : typeof r === 'object' && typeof r.url === 'string' 91 | ? r.url 92 | : null 93 | ) 94 | 95 | if (!rurl) { 96 | throw new Error('no repository') 97 | } 98 | 99 | const info = (rurl && GitHost.fromUrl(rurl.replace(/^git\+/, ''), opts)) || null 100 | if (info) { 101 | return info 102 | } 103 | const unk = unknownHostedUrl(rurl) 104 | return GitHost.fromUrl(unk, opts) || unk 105 | } 106 | 107 | static parseUrl (url) { 108 | return parseUrl(url) 109 | } 110 | 111 | #fill (template, opts) { 112 | if (typeof template !== 'function') { 113 | return null 114 | } 115 | 116 | const options = { ...this, ...this.opts, ...opts } 117 | 118 | // the path should always be set so we don't end up with 'undefined' in urls 119 | if (!options.path) { 120 | options.path = '' 121 | } 122 | 123 | // template functions will insert the leading slash themselves 124 | if (options.path.startsWith('/')) { 125 | options.path = options.path.slice(1) 126 | } 127 | 128 | if (options.noCommittish) { 129 | options.committish = null 130 | } 131 | 132 | const result = template(options) 133 | return options.noGitPlus && result.startsWith('git+') ? result.slice(4) : result 134 | } 135 | 136 | hash () { 137 | return this.committish ? `#${this.committish}` : '' 138 | } 139 | 140 | ssh (opts) { 141 | return this.#fill(this.sshtemplate, opts) 142 | } 143 | 144 | sshurl (opts) { 145 | return this.#fill(this.sshurltemplate, opts) 146 | } 147 | 148 | browse (path, ...args) { 149 | // not a string, treat path as opts 150 | if (typeof path !== 'string') { 151 | return this.#fill(this.browsetemplate, path) 152 | } 153 | 154 | if (typeof args[0] !== 'string') { 155 | return this.#fill(this.browsetreetemplate, { ...args[0], path }) 156 | } 157 | 158 | return this.#fill(this.browsetreetemplate, { ...args[1], fragment: args[0], path }) 159 | } 160 | 161 | // If the path is known to be a file, then browseFile should be used. For some hosts 162 | // the url is the same as browse, but for others like GitHub a file can use both `/tree/` 163 | // and `/blob/` in the path. When using a default committish of `HEAD` then the `/tree/` 164 | // path will redirect to a specific commit. Using the `/blob/` path avoids this and 165 | // does not redirect to a different commit. 166 | browseFile (path, ...args) { 167 | if (typeof args[0] !== 'string') { 168 | return this.#fill(this.browseblobtemplate, { ...args[0], path }) 169 | } 170 | 171 | return this.#fill(this.browseblobtemplate, { ...args[1], fragment: args[0], path }) 172 | } 173 | 174 | docs (opts) { 175 | return this.#fill(this.docstemplate, opts) 176 | } 177 | 178 | bugs (opts) { 179 | return this.#fill(this.bugstemplate, opts) 180 | } 181 | 182 | https (opts) { 183 | return this.#fill(this.httpstemplate, opts) 184 | } 185 | 186 | git (opts) { 187 | return this.#fill(this.gittemplate, opts) 188 | } 189 | 190 | shortcut (opts) { 191 | return this.#fill(this.shortcuttemplate, opts) 192 | } 193 | 194 | path (opts) { 195 | return this.#fill(this.pathtemplate, opts) 196 | } 197 | 198 | tarball (opts) { 199 | return this.#fill(this.tarballtemplate, { ...opts, noCommittish: false }) 200 | } 201 | 202 | file (path, opts) { 203 | return this.#fill(this.filetemplate, { ...opts, path }) 204 | } 205 | 206 | edit (path, opts) { 207 | return this.#fill(this.edittemplate, { ...opts, path }) 208 | } 209 | 210 | getDefaultRepresentation () { 211 | return this.default 212 | } 213 | 214 | toString (opts) { 215 | if (this.default && typeof this[this.default] === 'function') { 216 | return this[this.default](opts) 217 | } 218 | 219 | return this.sshurl(opts) 220 | } 221 | } 222 | 223 | for (const [name, host] of Object.entries(hosts)) { 224 | GitHost.addHost(name, host) 225 | } 226 | 227 | module.exports = GitHost 228 | -------------------------------------------------------------------------------- /lib/parse-url.js: -------------------------------------------------------------------------------- 1 | const url = require('url') 2 | 3 | const lastIndexOfBefore = (str, char, beforeChar) => { 4 | const startPosition = str.indexOf(beforeChar) 5 | return str.lastIndexOf(char, startPosition > -1 ? startPosition : Infinity) 6 | } 7 | 8 | const safeUrl = (u) => { 9 | try { 10 | return new url.URL(u) 11 | } catch { 12 | // this fn should never throw 13 | } 14 | } 15 | 16 | // accepts input like git:github.com:user/repo and inserts the // after the first : 17 | const correctProtocol = (arg, protocols) => { 18 | const firstColon = arg.indexOf(':') 19 | const proto = arg.slice(0, firstColon + 1) 20 | if (Object.prototype.hasOwnProperty.call(protocols, proto)) { 21 | return arg 22 | } 23 | 24 | const firstAt = arg.indexOf('@') 25 | if (firstAt > -1) { 26 | if (firstAt > firstColon) { 27 | return `git+ssh://${arg}` 28 | } else { 29 | return arg 30 | } 31 | } 32 | 33 | const doubleSlash = arg.indexOf('//') 34 | if (doubleSlash === firstColon + 1) { 35 | return arg 36 | } 37 | 38 | return `${arg.slice(0, firstColon + 1)}//${arg.slice(firstColon + 1)}` 39 | } 40 | 41 | // attempt to correct an scp style url so that it will parse with `new URL()` 42 | const correctUrl = (giturl) => { 43 | // ignore @ that come after the first hash since the denotes the start 44 | // of a committish which can contain @ characters 45 | const firstAt = lastIndexOfBefore(giturl, '@', '#') 46 | // ignore colons that come after the hash since that could include colons such as: 47 | // git@github.com:user/package-2#semver:^1.0.0 48 | const lastColonBeforeHash = lastIndexOfBefore(giturl, ':', '#') 49 | 50 | if (lastColonBeforeHash > firstAt) { 51 | // the last : comes after the first @ (or there is no @) 52 | // like it would in: 53 | // proto://hostname.com:user/repo 54 | // username@hostname.com:user/repo 55 | // :password@hostname.com:user/repo 56 | // username:password@hostname.com:user/repo 57 | // proto://username@hostname.com:user/repo 58 | // proto://:password@hostname.com:user/repo 59 | // proto://username:password@hostname.com:user/repo 60 | // then we replace the last : with a / to create a valid path 61 | giturl = giturl.slice(0, lastColonBeforeHash) + '/' + giturl.slice(lastColonBeforeHash + 1) 62 | } 63 | 64 | if (lastIndexOfBefore(giturl, ':', '#') === -1 && giturl.indexOf('//') === -1) { 65 | // we have no : at all 66 | // as it would be in: 67 | // username@hostname.com/user/repo 68 | // then we prepend a protocol 69 | giturl = `git+ssh://${giturl}` 70 | } 71 | 72 | return giturl 73 | } 74 | 75 | module.exports = (giturl, protocols) => { 76 | const withProtocol = protocols ? correctProtocol(giturl, protocols) : giturl 77 | return safeUrl(withProtocol) || safeUrl(correctUrl(withProtocol)) 78 | } 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hosted-git-info", 3 | "version": "8.1.0", 4 | "description": "Provides metadata and conversions from repository urls for GitHub, Bitbucket and GitLab", 5 | "main": "./lib/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/npm/hosted-git-info.git" 9 | }, 10 | "keywords": [ 11 | "git", 12 | "github", 13 | "bitbucket", 14 | "gitlab" 15 | ], 16 | "author": "GitHub Inc.", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/npm/hosted-git-info/issues" 20 | }, 21 | "homepage": "https://github.com/npm/hosted-git-info", 22 | "scripts": { 23 | "posttest": "npm run lint", 24 | "snap": "tap", 25 | "test": "tap", 26 | "test:coverage": "tap --coverage-report=html", 27 | "lint": "npm run eslint", 28 | "postlint": "template-oss-check", 29 | "lintfix": "npm run eslint -- --fix", 30 | "template-oss-apply": "template-oss-apply --force", 31 | "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"" 32 | }, 33 | "dependencies": { 34 | "lru-cache": "^10.0.1" 35 | }, 36 | "devDependencies": { 37 | "@npmcli/eslint-config": "^5.0.0", 38 | "@npmcli/template-oss": "4.24.3", 39 | "tap": "^16.0.1" 40 | }, 41 | "files": [ 42 | "bin/", 43 | "lib/" 44 | ], 45 | "engines": { 46 | "node": "^18.17.0 || >=20.5.0" 47 | }, 48 | "tap": { 49 | "color": 1, 50 | "coverage": true, 51 | "nyc-arg": [ 52 | "--exclude", 53 | "tap-snapshots/**" 54 | ] 55 | }, 56 | "templateOSS": { 57 | "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", 58 | "version": "4.24.3", 59 | "publish": "true" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "group-pull-request-title-pattern": "chore: release ${version}", 3 | "pull-request-title-pattern": "chore: release${component} ${version}", 4 | "changelog-sections": [ 5 | { 6 | "type": "feat", 7 | "section": "Features", 8 | "hidden": false 9 | }, 10 | { 11 | "type": "fix", 12 | "section": "Bug Fixes", 13 | "hidden": false 14 | }, 15 | { 16 | "type": "docs", 17 | "section": "Documentation", 18 | "hidden": false 19 | }, 20 | { 21 | "type": "deps", 22 | "section": "Dependencies", 23 | "hidden": false 24 | }, 25 | { 26 | "type": "chore", 27 | "section": "Chores", 28 | "hidden": true 29 | } 30 | ], 31 | "packages": { 32 | ".": { 33 | "package-name": "" 34 | } 35 | }, 36 | "prerelease-type": "pre.0" 37 | } 38 | -------------------------------------------------------------------------------- /test/bitbucket.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | 'use strict' 3 | const HostedGit = require('..') 4 | const t = require('tap') 5 | 6 | const invalid = [ 7 | // invalid protocol 8 | 'git://bitbucket.org/foo/bar', 9 | // url to get a tarball 10 | 'https://bitbucket.org/foo/bar/get/archive.tar.gz', 11 | // missing project 12 | 'https://bitbucket.org/foo', 13 | ] 14 | 15 | const defaults = { type: 'bitbucket', user: 'foo', project: 'bar' } 16 | 17 | const valid = { 18 | // shortucts 19 | // 20 | // NOTE auth is accepted but ignored 21 | 'bitbucket:foo/bar': { ...defaults, default: 'shortcut' }, 22 | 'bitbucket:foo/bar#branch': { ...defaults, default: 'shortcut', committish: 'branch' }, 23 | 'bitbucket:user@foo/bar': { ...defaults, default: 'shortcut', auth: null }, 24 | 'bitbucket:user@foo/bar#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 25 | 'bitbucket:user:password@foo/bar': { ...defaults, default: 'shortcut', auth: null }, 26 | 'bitbucket:user:password@foo/bar#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 27 | 'bitbucket::password@foo/bar': { ...defaults, default: 'shortcut', auth: null }, 28 | 'bitbucket::password@foo/bar#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 29 | 30 | 'bitbucket:foo/bar.git': { ...defaults, default: 'shortcut' }, 31 | 'bitbucket:foo/bar.git#branch': { ...defaults, default: 'shortcut', committish: 'branch' }, 32 | 'bitbucket:user@foo/bar.git': { ...defaults, default: 'shortcut', auth: null }, 33 | 'bitbucket:user@foo/bar.git#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 34 | 'bitbucket:user:password@foo/bar.git': { ...defaults, default: 'shortcut', auth: null }, 35 | 'bitbucket:user:password@foo/bar.git#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 36 | 'bitbucket::password@foo/bar.git': { ...defaults, default: 'shortcut', auth: null }, 37 | 'bitbucket::password@foo/bar.git#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 38 | 39 | // no-protocol git+ssh 40 | // 41 | // NOTE auth is accepted but ignored 42 | 'git@bitbucket.org:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 43 | 'git@bitbucket.org:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 44 | 'user@bitbucket.org:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 45 | 'user@bitbucket.org:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 46 | 'user:password@bitbucket.org:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 47 | 'user:password@bitbucket.org:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 48 | ':password@bitbucket.org:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 49 | ':password@bitbucket.org:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 50 | 51 | 'git@bitbucket.org:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 52 | 'git@bitbucket.org:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 53 | 'user@bitbucket.org:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 54 | 'user@bitbucket.org:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 55 | 'user:password@bitbucket.org:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 56 | 'user:password@bitbucket.org:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 57 | ':password@bitbucket.org:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 58 | ':password@bitbucket.org:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 59 | 60 | // git+ssh urls 61 | // 62 | // NOTE auth is accepted but ignored 63 | 'git+ssh://bitbucket.org:foo/bar': { ...defaults, default: 'sshurl' }, 64 | 'git+ssh://bitbucket.org:foo/bar#branch': { ...defaults, default: 'sshurl', committish: 'branch' }, 65 | 'git+ssh://user@bitbucket.org:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 66 | 'git+ssh://user@bitbucket.org:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 67 | 'git+ssh://user:password@bitbucket.org:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 68 | 'git+ssh://user:password@bitbucket.org:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 69 | 'git+ssh://:password@bitbucket.org:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 70 | 'git+ssh://:password@bitbucket.org:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 71 | 72 | 'git+ssh://bitbucket.org:foo/bar.git': { ...defaults, default: 'sshurl' }, 73 | 'git+ssh://bitbucket.org:foo/bar.git#branch': { ...defaults, default: 'sshurl', committish: 'branch' }, 74 | 'git+ssh://user@bitbucket.org:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 75 | 'git+ssh://user@bitbucket.org:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 76 | 'git+ssh://user:password@bitbucket.org:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 77 | 'git+ssh://user:password@bitbucket.org:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 78 | 'git+ssh://:password@bitbucket.org:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 79 | 'git+ssh://:password@bitbucket.org:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 80 | 81 | // ssh urls 82 | // 83 | // NOTE auth is accepted but ignored 84 | 'ssh://bitbucket.org:foo/bar': { ...defaults, default: 'sshurl' }, 85 | 'ssh://bitbucket.org:foo/bar#branch': { ...defaults, default: 'sshurl', committish: 'branch' }, 86 | 'ssh://user@bitbucket.org:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 87 | 'ssh://user@bitbucket.org:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 88 | 'ssh://user:password@bitbucket.org:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 89 | 'ssh://user:password@bitbucket.org:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 90 | 'ssh://:password@bitbucket.org:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 91 | 'ssh://:password@bitbucket.org:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 92 | 93 | 'ssh://bitbucket.org:foo/bar.git': { ...defaults, default: 'sshurl' }, 94 | 'ssh://bitbucket.org:foo/bar.git#branch': { ...defaults, default: 'sshurl', committish: 'branch' }, 95 | 'ssh://user@bitbucket.org:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 96 | 'ssh://user@bitbucket.org:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 97 | 'ssh://user:password@bitbucket.org:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 98 | 'ssh://user:password@bitbucket.org:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 99 | 'ssh://:password@bitbucket.org:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 100 | 'ssh://:password@bitbucket.org:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 101 | 102 | // git+https urls 103 | // 104 | // NOTE auth is accepted and respected 105 | 'git+https://bitbucket.org/foo/bar': { ...defaults, default: 'https' }, 106 | 'git+https://bitbucket.org/foo/bar#branch': { ...defaults, default: 'https', committish: 'branch' }, 107 | 'git+https://user@bitbucket.org/foo/bar': { ...defaults, default: 'https', auth: 'user' }, 108 | 'git+https://user@bitbucket.org/foo/bar#branch': { ...defaults, default: 'https', auth: 'user', committish: 'branch' }, 109 | 'git+https://user:password@bitbucket.org/foo/bar': { ...defaults, default: 'https', auth: 'user:password' }, 110 | 'git+https://user:password@bitbucket.org/foo/bar#branch': { ...defaults, default: 'https', auth: 'user:password', committish: 'branch' }, 111 | 'git+https://:password@bitbucket.org/foo/bar': { ...defaults, default: 'https', auth: ':password' }, 112 | 'git+https://:password@bitbucket.org/foo/bar#branch': { ...defaults, default: 'https', auth: ':password', committish: 'branch' }, 113 | 114 | 'git+https://bitbucket.org/foo/bar.git': { ...defaults, default: 'https' }, 115 | 'git+https://bitbucket.org/foo/bar.git#branch': { ...defaults, default: 'https', committish: 'branch' }, 116 | 'git+https://user@bitbucket.org/foo/bar.git': { ...defaults, default: 'https', auth: 'user' }, 117 | 'git+https://user@bitbucket.org/foo/bar.git#branch': { ...defaults, default: 'https', auth: 'user', committish: 'branch' }, 118 | 'git+https://user:password@bitbucket.org/foo/bar.git': { ...defaults, default: 'https', auth: 'user:password' }, 119 | 'git+https://user:password@bitbucket.org/foo/bar.git#branch': { ...defaults, default: 'https', auth: 'user:password', committish: 'branch' }, 120 | 'git+https://:password@bitbucket.org/foo/bar.git': { ...defaults, default: 'https', auth: ':password' }, 121 | 'git+https://:password@bitbucket.org/foo/bar.git#branch': { ...defaults, default: 'https', auth: ':password', committish: 'branch' }, 122 | 123 | // https urls 124 | // 125 | // NOTE auth is accepted and respected 126 | 'https://bitbucket.org/foo/bar': { ...defaults, default: 'https' }, 127 | 'https://bitbucket.org/foo/bar#branch': { ...defaults, default: 'https', committish: 'branch' }, 128 | 'https://user@bitbucket.org/foo/bar': { ...defaults, default: 'https', auth: 'user' }, 129 | 'https://user@bitbucket.org/foo/bar#branch': { ...defaults, default: 'https', auth: 'user', committish: 'branch' }, 130 | 'https://user:password@bitbucket.org/foo/bar': { ...defaults, default: 'https', auth: 'user:password' }, 131 | 'https://user:password@bitbucket.org/foo/bar#branch': { ...defaults, default: 'https', auth: 'user:password', committish: 'branch' }, 132 | 'https://:password@bitbucket.org/foo/bar': { ...defaults, default: 'https', auth: ':password' }, 133 | 'https://:password@bitbucket.org/foo/bar#branch': { ...defaults, default: 'https', auth: ':password', committish: 'branch' }, 134 | 135 | 'https://bitbucket.org/foo/bar.git': { ...defaults, default: 'https' }, 136 | 'https://bitbucket.org/foo/bar.git#branch': { ...defaults, default: 'https', committish: 'branch' }, 137 | 'https://user@bitbucket.org/foo/bar.git': { ...defaults, default: 'https', auth: 'user' }, 138 | 'https://user@bitbucket.org/foo/bar.git#branch': { ...defaults, default: 'https', auth: 'user', committish: 'branch' }, 139 | 'https://user:password@bitbucket.org/foo/bar.git': { ...defaults, default: 'https', auth: 'user:password' }, 140 | 'https://user:password@bitbucket.org/foo/bar.git#branch': { ...defaults, default: 'https', auth: 'user:password', committish: 'branch' }, 141 | 'https://:password@bitbucket.org/foo/bar.git': { ...defaults, default: 'https', auth: ':password' }, 142 | 'https://:password@bitbucket.org/foo/bar.git#branch': { ...defaults, default: 'https', auth: ':password', committish: 'branch' }, 143 | } 144 | 145 | t.test('valid urls parse properly', t => { 146 | t.plan(Object.keys(valid).length) 147 | for (const [url, result] of Object.entries(valid)) { 148 | t.hasStrict(HostedGit.fromUrl(url), result, `${url} parses`) 149 | } 150 | }) 151 | 152 | t.test('invalid urls return undefined', t => { 153 | t.plan(invalid.length) 154 | for (const url of invalid) { 155 | t.equal(HostedGit.fromUrl(url), undefined, `${url} returns undefined`) 156 | } 157 | }) 158 | 159 | t.test('toString respects defaults', t => { 160 | const sshurl = HostedGit.fromUrl('git+ssh://bitbucket.org/foo/bar') 161 | t.equal(sshurl.default, 'sshurl', 'got the right default') 162 | t.equal(sshurl.toString(), sshurl.sshurl(), 'toString calls sshurl') 163 | 164 | const https = HostedGit.fromUrl('https://bitbucket.org/foo/bar') 165 | t.equal(https.default, 'https', 'got the right default') 166 | t.equal(https.toString(), https.https(), 'toString calls https') 167 | 168 | const shortcut = HostedGit.fromUrl('bitbucket:foo/bar') 169 | t.equal(shortcut.default, 'shortcut', 'got the right default') 170 | t.equal(shortcut.toString(), shortcut.shortcut(), 'toString calls shortcut') 171 | 172 | t.end() 173 | }) 174 | 175 | t.test('string methods populate correctly', t => { 176 | const parsed = HostedGit.fromUrl('git+ssh://bitbucket.org/foo/bar') 177 | t.equal(parsed.getDefaultRepresentation(), parsed.default, 'getDefaultRepresentation()') 178 | t.equal(parsed.hash(), '', 'hash() returns empty string when committish is unset') 179 | t.equal(parsed.ssh(), 'git@bitbucket.org:foo/bar.git') 180 | t.equal(parsed.sshurl(), 'git+ssh://git@bitbucket.org/foo/bar.git') 181 | t.equal(parsed.edit(), 'https://bitbucket.org/foo/bar') 182 | t.equal(parsed.edit('/lib/index.js'), 'https://bitbucket.org/foo/bar/src/HEAD/lib/index.js?mode=edit') 183 | t.equal(parsed.browse(), 'https://bitbucket.org/foo/bar') 184 | t.equal(parsed.browse('/lib/index.js'), 'https://bitbucket.org/foo/bar/src/HEAD/lib/index.js') 185 | t.equal(parsed.browse('/lib/index.js', 'L100'), 'https://bitbucket.org/foo/bar/src/HEAD/lib/index.js#l100') 186 | t.equal(parsed.docs(), 'https://bitbucket.org/foo/bar#readme') 187 | t.equal(parsed.https(), 'git+https://bitbucket.org/foo/bar.git') 188 | t.equal(parsed.shortcut(), 'bitbucket:foo/bar') 189 | t.equal(parsed.path(), 'foo/bar') 190 | t.equal(parsed.tarball(), 'https://bitbucket.org/foo/bar/get/HEAD.tar.gz') 191 | t.equal(parsed.file(), 'https://bitbucket.org/foo/bar/raw/HEAD/') 192 | t.equal(parsed.file('/lib/index.js'), 'https://bitbucket.org/foo/bar/raw/HEAD/lib/index.js') 193 | t.equal(parsed.bugs(), 'https://bitbucket.org/foo/bar/issues') 194 | 195 | t.equal(parsed.docs({ committish: 'fix/bug' }), 'https://bitbucket.org/foo/bar/src/fix%2Fbug#readme', 'allows overriding options') 196 | 197 | t.same(parsed.git(), null, 'git() returns null') 198 | 199 | const extra = HostedGit.fromUrl('https://user@bitbucket.org/foo/bar#fix/bug') 200 | t.equal(extra.hash(), '#fix/bug') 201 | t.equal(extra.https(), 'git+https://user@bitbucket.org/foo/bar.git#fix/bug') 202 | t.equal(extra.shortcut(), 'bitbucket:foo/bar#fix/bug') 203 | t.equal(extra.ssh(), 'git@bitbucket.org:foo/bar.git#fix/bug') 204 | t.equal(extra.sshurl(), 'git+ssh://git@bitbucket.org/foo/bar.git#fix/bug') 205 | t.equal(extra.browse(), 'https://bitbucket.org/foo/bar/src/fix%2Fbug') 206 | t.equal(extra.browse('/lib/index.js'), 'https://bitbucket.org/foo/bar/src/fix%2Fbug/lib/index.js') 207 | t.equal(extra.browse('/lib/index.js', 'L200'), 'https://bitbucket.org/foo/bar/src/fix%2Fbug/lib/index.js#l200') 208 | t.equal(extra.docs(), 'https://bitbucket.org/foo/bar/src/fix%2Fbug#readme') 209 | t.equal(extra.file(), 'https://bitbucket.org/foo/bar/raw/fix%2Fbug/') 210 | t.equal(extra.file('/lib/index.js'), 'https://bitbucket.org/foo/bar/raw/fix%2Fbug/lib/index.js') 211 | 212 | t.equal(extra.sshurl({ noCommittish: true }), 'git+ssh://git@bitbucket.org/foo/bar.git', 'noCommittish drops committish from urls') 213 | t.equal(extra.sshurl({ noGitPlus: true }), 'ssh://git@bitbucket.org/foo/bar.git#fix/bug', 'noGitPlus drops git+ prefix from urls') 214 | 215 | t.end() 216 | }) 217 | -------------------------------------------------------------------------------- /test/file.js: -------------------------------------------------------------------------------- 1 | const HostedGit = require('..') 2 | const t = require('tap') 3 | 4 | t.test('file:// URLs', t => { 5 | const fileRepo = { 6 | name: 'foo', 7 | repository: { 8 | url: 'file:///path/dot.git', 9 | }, 10 | } 11 | t.equal(HostedGit.fromManifest(fileRepo), null) 12 | 13 | t.end() 14 | }) 15 | -------------------------------------------------------------------------------- /test/gist.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | 'use strict' 3 | const HostedGit = require('..') 4 | const t = require('tap') 5 | 6 | const invalid = [ 7 | // raw urls that are wrong anyway but for some reason are in the wild 8 | 'https://gist.github.com/foo/feedbeef/raw/fix%2Fbug/', 9 | // missing both user and project 10 | 'https://gist.github.com/', 11 | ] 12 | 13 | const defaults = { type: 'gist', user: null, project: 'feedbeef' } 14 | const valid = { 15 | // shortcuts 16 | // 17 | // NOTE auth is accepted but ignored 18 | 'gist:feedbeef': { ...defaults, default: 'shortcut' }, 19 | 'gist:feedbeef#branch': { ...defaults, default: 'shortcut', committish: 'branch' }, 20 | 'gist:user@feedbeef': { ...defaults, default: 'shortcut', auth: null }, 21 | 'gist:user@feedbeef#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 22 | 'gist:user:password@feedbeef': { ...defaults, default: 'shortcut', auth: null }, 23 | 'gist:user:password@feedbeef#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 24 | 'gist::password@feedbeef': { ...defaults, default: 'shortcut', auth: null }, 25 | 'gist::password@feedbeef#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 26 | 27 | 'gist:feedbeef.git': { ...defaults, default: 'shortcut' }, 28 | 'gist:feedbeef.git#branch': { ...defaults, default: 'shortcut', committish: 'branch' }, 29 | 'gist:user@feedbeef.git': { ...defaults, default: 'shortcut', auth: null }, 30 | 'gist:user@feedbeef.git#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 31 | 'gist:user:password@feedbeef.git': { ...defaults, default: 'shortcut', auth: null }, 32 | 'gist:user:password@feedbeef.git#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 33 | 'gist::password@feedbeef.git': { ...defaults, default: 'shortcut', auth: null }, 34 | 'gist::password@feedbeef.git#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 35 | 36 | 'gist:/feedbeef': { ...defaults, default: 'shortcut' }, 37 | 'gist:/feedbeef#branch': { ...defaults, default: 'shortcut', committish: 'branch' }, 38 | 'gist:user@/feedbeef': { ...defaults, default: 'shortcut', auth: null }, 39 | 'gist:user@/feedbeef#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 40 | 'gist:user:password@/feedbeef': { ...defaults, default: 'shortcut', auth: null }, 41 | 'gist:user:password@/feedbeef#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 42 | 'gist::password@/feedbeef': { ...defaults, default: 'shortcut', auth: null }, 43 | 'gist::password@/feedbeef#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 44 | 45 | 'gist:/feedbeef.git': { ...defaults, default: 'shortcut' }, 46 | 'gist:/feedbeef.git#branch': { ...defaults, default: 'shortcut', committish: 'branch' }, 47 | 'gist:user@/feedbeef.git': { ...defaults, default: 'shortcut', auth: null }, 48 | 'gist:user@/feedbeef.git#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 49 | 'gist:user:password@/feedbeef.git': { ...defaults, default: 'shortcut', auth: null }, 50 | 'gist:user:password@/feedbeef.git#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 51 | 'gist::password@/feedbeef.git': { ...defaults, default: 'shortcut', auth: null }, 52 | 'gist::password@/feedbeef.git#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 53 | 54 | 'gist:foo/feedbeef': { ...defaults, default: 'shortcut', user: 'foo' }, 55 | 'gist:foo/feedbeef#branch': { ...defaults, default: 'shortcut', user: 'foo', committish: 'branch' }, 56 | 'gist:user@foo/feedbeef': { ...defaults, default: 'shortcut', user: 'foo', auth: null }, 57 | 'gist:user@foo/feedbeef#branch': { ...defaults, default: 'shortcut', user: 'foo', auth: null, committish: 'branch' }, 58 | 'gist:user:password@foo/feedbeef': { ...defaults, default: 'shortcut', user: 'foo', auth: null }, 59 | 'gist:user:password@foo/feedbeef#branch': { ...defaults, default: 'shortcut', user: 'foo', auth: null, committish: 'branch' }, 60 | 'gist::password@foo/feedbeef': { ...defaults, default: 'shortcut', user: 'foo', auth: null }, 61 | 'gist::password@foo/feedbeef#branch': { ...defaults, default: 'shortcut', user: 'foo', auth: null, committish: 'branch' }, 62 | 63 | 'gist:foo/feedbeef.git': { ...defaults, default: 'shortcut', user: 'foo' }, 64 | 'gist:foo/feedbeef.git#branch': { ...defaults, default: 'shortcut', user: 'foo', committish: 'branch' }, 65 | 'gist:user@foo/feedbeef.git': { ...defaults, default: 'shortcut', user: 'foo', auth: null }, 66 | 'gist:user@foo/feedbeef.git#branch': { ...defaults, default: 'shortcut', user: 'foo', auth: null, committish: 'branch' }, 67 | 'gist:user:password@foo/feedbeef.git': { ...defaults, default: 'shortcut', user: 'foo', auth: null }, 68 | 'gist:user:password@foo/feedbeef.git#branch': { ...defaults, default: 'shortcut', user: 'foo', auth: null, committish: 'branch' }, 69 | 'gist::password@foo/feedbeef.git': { ...defaults, default: 'shortcut', user: 'foo', auth: null }, 70 | 'gist::password@foo/feedbeef.git#branch': { ...defaults, default: 'shortcut', user: 'foo', auth: null, committish: 'branch' }, 71 | 72 | // git urls 73 | // 74 | // NOTE auth is accepted and respected 75 | 'git://gist.github.com/feedbeef': { ...defaults, default: 'git' }, 76 | 'git://gist.github.com/feedbeef#branch': { ...defaults, default: 'git', committish: 'branch' }, 77 | 'git://user@gist.github.com/feedbeef': { ...defaults, default: 'git', auth: 'user' }, 78 | 'git://user@gist.github.com/feedbeef#branch': { ...defaults, default: 'git', auth: 'user', committish: 'branch' }, 79 | 'git://user:password@gist.github.com/feedbeef': { ...defaults, default: 'git', auth: 'user:password' }, 80 | 'git://user:password@gist.github.com/feedbeef#branch': { ...defaults, default: 'git', auth: 'user:password', committish: 'branch' }, 81 | 'git://:password@gist.github.com/feedbeef': { ...defaults, default: 'git', auth: ':password' }, 82 | 'git://:password@gist.github.com/feedbeef#branch': { ...defaults, default: 'git', auth: ':password', committish: 'branch' }, 83 | 84 | 'git://gist.github.com/feedbeef.git': { ...defaults, default: 'git' }, 85 | 'git://gist.github.com/feedbeef.git#branch': { ...defaults, default: 'git', committish: 'branch' }, 86 | 'git://user@gist.github.com/feedbeef.git': { ...defaults, default: 'git', auth: 'user' }, 87 | 'git://user@gist.github.com/feedbeef.git#branch': { ...defaults, default: 'git', auth: 'user', committish: 'branch' }, 88 | 'git://user:password@gist.github.com/feedbeef.git': { ...defaults, default: 'git', auth: 'user:password' }, 89 | 'git://user:password@gist.github.com/feedbeef.git#branch': { ...defaults, default: 'git', auth: 'user:password', committish: 'branch' }, 90 | 'git://:password@gist.github.com/feedbeef.git': { ...defaults, default: 'git', auth: ':password' }, 91 | 'git://:password@gist.github.com/feedbeef.git#branch': { ...defaults, default: 'git', auth: ':password', committish: 'branch' }, 92 | 93 | 'git://gist.github.com/foo/feedbeef': { ...defaults, default: 'git', user: 'foo' }, 94 | 'git://gist.github.com/foo/feedbeef#branch': { ...defaults, default: 'git', user: 'foo', committish: 'branch' }, 95 | 'git://user@gist.github.com/foo/feedbeef': { ...defaults, default: 'git', user: 'foo', auth: 'user' }, 96 | 'git://user@gist.github.com/foo/feedbeef#branch': { ...defaults, default: 'git', user: 'foo', auth: 'user', committish: 'branch' }, 97 | 'git://user:password@gist.github.com/foo/feedbeef': { ...defaults, default: 'git', user: 'foo', auth: 'user:password' }, 98 | 'git://user:password@gist.github.com/foo/feedbeef#branch': { ...defaults, default: 'git', user: 'foo', auth: 'user:password', committish: 'branch' }, 99 | 'git://:password@gist.github.com/foo/feedbeef': { ...defaults, default: 'git', user: 'foo', auth: ':password' }, 100 | 'git://:password@gist.github.com/foo/feedbeef#branch': { ...defaults, default: 'git', user: 'foo', auth: ':password', committish: 'branch' }, 101 | 102 | 'git://gist.github.com/foo/feedbeef.git': { ...defaults, default: 'git', user: 'foo' }, 103 | 'git://gist.github.com/foo/feedbeef.git#branch': { ...defaults, default: 'git', user: 'foo', committish: 'branch' }, 104 | 'git://user@gist.github.com/foo/feedbeef.git': { ...defaults, default: 'git', user: 'foo', auth: 'user' }, 105 | 'git://user@gist.github.com/foo/feedbeef.git#branch': { ...defaults, default: 'git', user: 'foo', auth: 'user', committish: 'branch' }, 106 | 'git://user:password@gist.github.com/foo/feedbeef.git': { ...defaults, default: 'git', user: 'foo', auth: 'user:password' }, 107 | 'git://user:password@gist.github.com/foo/feedbeef.git#branch': { ...defaults, default: 'git', user: 'foo', auth: 'user:password', committish: 'branch' }, 108 | 'git://:password@gist.github.com/foo/feedbeef.git': { ...defaults, default: 'git', user: 'foo', auth: ':password' }, 109 | 'git://:password@gist.github.com/foo/feedbeef.git#branch': { ...defaults, default: 'git', user: 'foo', auth: ':password', committish: 'branch' }, 110 | 111 | // no-protocol git+ssh 112 | // 113 | // NOTE auth is accepted and ignored 114 | 'git@gist.github.com:feedbeef': { ...defaults, default: 'sshurl', auth: null }, 115 | 'git@gist.github.com:feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 116 | 'user@gist.github.com:feedbeef': { ...defaults, default: 'sshurl', auth: null }, 117 | 'user@gist.github.com:feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 118 | 'user:password@gist.github.com:feedbeef': { ...defaults, default: 'sshurl', auth: null }, 119 | 'user:password@gist.github.com:feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 120 | ':password@gist.github.com:feedbeef': { ...defaults, default: 'sshurl', auth: null }, 121 | ':password@gist.github.com:feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 122 | 123 | 'git@gist.github.com:feedbeef.git': { ...defaults, default: 'sshurl', auth: null }, 124 | 'git@gist.github.com:feedbeef.git#branch': { ...defaults, default: 'sshurl', committish: 'branch', auth: null }, 125 | 'user@gist.github.com:feedbeef.git': { ...defaults, default: 'sshurl', auth: null }, 126 | 'user@gist.github.com:feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 127 | 'user:password@gist.github.com:feedbeef.git': { ...defaults, default: 'sshurl', auth: null }, 128 | 'user:password@gist.github.com:feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 129 | ':password@gist.github.com:feedbeef.git': { ...defaults, default: 'sshurl', auth: null }, 130 | ':password@gist.github.com:feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 131 | 132 | 'git@gist.github.com:foo/feedbeef': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 133 | 'git@gist.github.com:foo/feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 134 | 'user@gist.github.com:foo/feedbeef': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 135 | 'user@gist.github.com:foo/feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 136 | 'user:password@gist.github.com:foo/feedbeef': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 137 | 'user:password@gist.github.com:foo/feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 138 | ':password@gist.github.com:foo/feedbeef': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 139 | ':password@gist.github.com:foo/feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 140 | 141 | 'git@gist.github.com:foo/feedbeef.git': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 142 | 'git@gist.github.com:foo/feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 143 | 'user@gist.github.com:foo/feedbeef.git': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 144 | 'user@gist.github.com:foo/feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 145 | 'user:password@gist.github.com:foo/feedbeef.git': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 146 | 'user:password@gist.github.com:foo/feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 147 | ':password@gist.github.com:foo/feedbeef.git': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 148 | ':password@gist.github.com:foo/feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 149 | 150 | // git+ssh urls 151 | // 152 | // NOTE auth is accepted but ignored 153 | // NOTE see TODO at list of invalids, some inputs fail and shouldn't 154 | 'git+ssh://gist.github.com:feedbeef': { ...defaults, default: 'sshurl', auth: null }, 155 | 'git+ssh://gist.github.com:feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 156 | 'git+ssh://user@gist.github.com:feedbeef': { ...defaults, default: 'sshurl', auth: null }, 157 | 'git+ssh://user@gist.github.com:feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 158 | 'git+ssh://user:password@gist.github.com:feedbeef': { ...defaults, default: 'sshurl', auth: null }, 159 | 'git+ssh://user:password@gist.github.com:feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 160 | 'git+ssh://:password@gist.github.com:feedbeef': { ...defaults, default: 'sshurl', auth: null }, 161 | 'git+ssh://:password@gist.github.com:feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 162 | 163 | 'git+ssh://gist.github.com:feedbeef.git': { ...defaults, default: 'sshurl', auth: null }, 164 | 'git+ssh://gist.github.com:feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 165 | 'git+ssh://user@gist.github.com:feedbeef.git': { ...defaults, default: 'sshurl', auth: null }, 166 | 'git+ssh://user@gist.github.com:feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 167 | 'git+ssh://user:password@gist.github.com:feedbeef.git': { ...defaults, default: 'sshurl', auth: null }, 168 | 'git+ssh://user:password@gist.github.com:feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 169 | 'git+ssh://:password@gist.github.com:feedbeef.git': { ...defaults, default: 'sshurl', auth: null }, 170 | 'git+ssh://:password@gist.github.com:feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 171 | 172 | 'git+ssh://gist.github.com:foo/feedbeef': { ...defaults, default: 'sshurl', user: 'foo' }, 173 | 'git+ssh://gist.github.com:foo/feedbeef#branch': { ...defaults, default: 'sshurl', user: 'foo', committish: 'branch' }, 174 | 'git+ssh://user@gist.github.com:foo/feedbeef': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 175 | 'git+ssh://user@gist.github.com:foo/feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 176 | 'git+ssh://user:password@gist.github.com:foo/feedbeef': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 177 | 'git+ssh://user:password@gist.github.com:foo/feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 178 | 'git+ssh://:password@gist.github.com:foo/feedbeef': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 179 | 'git+ssh://:password@gist.github.com:foo/feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 180 | 181 | 'git+ssh://gist.github.com:foo/feedbeef.git': { ...defaults, default: 'sshurl', user: 'foo' }, 182 | 'git+ssh://gist.github.com:foo/feedbeef.git#branch': { ...defaults, default: 'sshurl', user: 'foo', committish: 'branch' }, 183 | 'git+ssh://user@gist.github.com:foo/feedbeef.git': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 184 | 'git+ssh://user@gist.github.com:foo/feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 185 | 'git+ssh://user:password@gist.github.com:foo/feedbeef.git': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 186 | 'git+ssh://user:password@gist.github.com:foo/feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 187 | 'git+ssh://:password@gist.github.com:foo/feedbeef.git': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 188 | 'git+ssh://:password@gist.github.com:foo/feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 189 | 190 | // ssh urls 191 | // 192 | // NOTE auth is accepted but ignored 193 | 'ssh://gist.github.com:feedbeef': { ...defaults, default: 'sshurl', auth: null }, 194 | 'ssh://gist.github.com:feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 195 | 'ssh://user@gist.github.com:feedbeef': { ...defaults, default: 'sshurl', auth: null }, 196 | 'ssh://user@gist.github.com:feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 197 | 'ssh://user:password@gist.github.com:feedbeef': { ...defaults, default: 'sshurl', auth: null }, 198 | 'ssh://user:password@gist.github.com:feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 199 | 'ssh://:password@gist.github.com:feedbeef': { ...defaults, default: 'sshurl', auth: null }, 200 | 'ssh://:password@gist.github.com:feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 201 | 202 | 'ssh://gist.github.com:feedbeef.git': { ...defaults, default: 'sshurl', auth: null }, 203 | 'ssh://gist.github.com:feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 204 | 'ssh://user@gist.github.com:feedbeef.git': { ...defaults, default: 'sshurl', auth: null }, 205 | 'ssh://user@gist.github.com:feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 206 | 'ssh://user:password@gist.github.com:feedbeef.git': { ...defaults, default: 'sshurl', auth: null }, 207 | 'ssh://user:password@gist.github.com:feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 208 | 'ssh://:password@gist.github.com:feedbeef.git': { ...defaults, default: 'sshurl', auth: null }, 209 | 'ssh://:password@gist.github.com:feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 210 | 211 | 'ssh://gist.github.com:foo/feedbeef': { ...defaults, default: 'sshurl', user: 'foo' }, 212 | 'ssh://gist.github.com:foo/feedbeef#branch': { ...defaults, default: 'sshurl', user: 'foo', committish: 'branch' }, 213 | 'ssh://user@gist.github.com:foo/feedbeef': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 214 | 'ssh://user@gist.github.com:foo/feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 215 | 'ssh://user:password@gist.github.com:foo/feedbeef': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 216 | 'ssh://user:password@gist.github.com:foo/feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 217 | 'ssh://:password@gist.github.com:foo/feedbeef': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 218 | 'ssh://:password@gist.github.com:foo/feedbeef#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 219 | 220 | 'ssh://gist.github.com:foo/feedbeef.git': { ...defaults, default: 'sshurl', user: 'foo' }, 221 | 'ssh://gist.github.com:foo/feedbeef.git#branch': { ...defaults, default: 'sshurl', user: 'foo', committish: 'branch' }, 222 | 'ssh://user@gist.github.com:foo/feedbeef.git': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 223 | 'ssh://user@gist.github.com:foo/feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 224 | 'ssh://user:password@gist.github.com:foo/feedbeef.git': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 225 | 'ssh://user:password@gist.github.com:foo/feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 226 | 'ssh://:password@gist.github.com:foo/feedbeef.git': { ...defaults, default: 'sshurl', auth: null, user: 'foo' }, 227 | 'ssh://:password@gist.github.com:foo/feedbeef.git#branch': { ...defaults, default: 'sshurl', auth: null, user: 'foo', committish: 'branch' }, 228 | 229 | // git+https urls 230 | // 231 | // NOTE auth is accepted and respected 232 | 'git+https://gist.github.com/feedbeef': { ...defaults, default: 'https' }, 233 | 'git+https://gist.github.com/feedbeef#branch': { ...defaults, default: 'https', committish: 'branch' }, 234 | 'git+https://user@gist.github.com/feedbeef': { ...defaults, default: 'https', auth: 'user' }, 235 | 'git+https://user@gist.github.com/feedbeef#branch': { ...defaults, default: 'https', auth: 'user', committish: 'branch' }, 236 | 'git+https://user:password@gist.github.com/feedbeef': { ...defaults, default: 'https', auth: 'user:password' }, 237 | 'git+https://user:password@gist.github.com/feedbeef#branch': { ...defaults, default: 'https', auth: 'user:password', committish: 'branch' }, 238 | 'git+https://:password@gist.github.com/feedbeef': { ...defaults, default: 'https', auth: ':password' }, 239 | 'git+https://:password@gist.github.com/feedbeef#branch': { ...defaults, default: 'https', auth: ':password', committish: 'branch' }, 240 | 241 | 'git+https://gist.github.com/feedbeef.git': { ...defaults, default: 'https' }, 242 | 'git+https://gist.github.com/feedbeef.git#branch': { ...defaults, default: 'https', committish: 'branch' }, 243 | 'git+https://user@gist.github.com/feedbeef.git': { ...defaults, default: 'https', auth: 'user' }, 244 | 'git+https://user@gist.github.com/feedbeef.git#branch': { ...defaults, default: 'https', auth: 'user', committish: 'branch' }, 245 | 'git+https://user:password@gist.github.com/feedbeef.git': { ...defaults, default: 'https', auth: 'user:password' }, 246 | 'git+https://user:password@gist.github.com/feedbeef.git#branch': { ...defaults, default: 'https', auth: 'user:password', committish: 'branch' }, 247 | 'git+https://:password@gist.github.com/feedbeef.git': { ...defaults, default: 'https', auth: ':password' }, 248 | 'git+https://:password@gist.github.com/feedbeef.git#branch': { ...defaults, default: 'https', auth: ':password', committish: 'branch' }, 249 | 250 | 'git+https://gist.github.com/foo/feedbeef': { ...defaults, default: 'https', user: 'foo' }, 251 | 'git+https://gist.github.com/foo/feedbeef#branch': { ...defaults, default: 'https', user: 'foo', committish: 'branch' }, 252 | 'git+https://user@gist.github.com/foo/feedbeef': { ...defaults, default: 'https', auth: 'user', user: 'foo' }, 253 | 'git+https://user@gist.github.com/foo/feedbeef#branch': { ...defaults, default: 'https', auth: 'user', user: 'foo', committish: 'branch' }, 254 | 'git+https://user:password@gist.github.com/foo/feedbeef': { ...defaults, default: 'https', auth: 'user:password', user: 'foo' }, 255 | 'git+https://user:password@gist.github.com/foo/feedbeef#branch': { ...defaults, default: 'https', auth: 'user:password', user: 'foo', committish: 'branch' }, 256 | 'git+https://:password@gist.github.com/foo/feedbeef': { ...defaults, default: 'https', auth: ':password', user: 'foo' }, 257 | 'git+https://:password@gist.github.com/foo/feedbeef#branch': { ...defaults, default: 'https', auth: ':password', user: 'foo', committish: 'branch' }, 258 | 259 | 'git+https://gist.github.com/foo/feedbeef.git': { ...defaults, default: 'https', user: 'foo' }, 260 | 'git+https://gist.github.com/foo/feedbeef.git#branch': { ...defaults, default: 'https', user: 'foo', committish: 'branch' }, 261 | 'git+https://user@gist.github.com/foo/feedbeef.git': { ...defaults, default: 'https', auth: 'user', user: 'foo' }, 262 | 'git+https://user@gist.github.com/foo/feedbeef.git#branch': { ...defaults, default: 'https', auth: 'user', user: 'foo', committish: 'branch' }, 263 | 'git+https://user:password@gist.github.com/foo/feedbeef.git': { ...defaults, default: 'https', auth: 'user:password', user: 'foo' }, 264 | 'git+https://user:password@gist.github.com/foo/feedbeef.git#branch': { ...defaults, default: 'https', auth: 'user:password', user: 'foo', committish: 'branch' }, 265 | 'git+https://:password@gist.github.com/foo/feedbeef.git': { ...defaults, default: 'https', auth: ':password', user: 'foo' }, 266 | 'git+https://:password@gist.github.com/foo/feedbeef.git#branch': { ...defaults, default: 'https', auth: ':password', user: 'foo', committish: 'branch' }, 267 | 268 | // https urls 269 | // 270 | // NOTE auth is accepted and respected 271 | 'https://gist.github.com/feedbeef': { ...defaults, default: 'https' }, 272 | 'https://gist.github.com/feedbeef#branch': { ...defaults, default: 'https', committish: 'branch' }, 273 | 'https://user@gist.github.com/feedbeef': { ...defaults, default: 'https', auth: 'user' }, 274 | 'https://user@gist.github.com/feedbeef#branch': { ...defaults, default: 'https', auth: 'user', committish: 'branch' }, 275 | 'https://user:password@gist.github.com/feedbeef': { ...defaults, default: 'https', auth: 'user:password' }, 276 | 'https://user:password@gist.github.com/feedbeef#branch': { ...defaults, default: 'https', auth: 'user:password', committish: 'branch' }, 277 | 'https://:password@gist.github.com/feedbeef': { ...defaults, default: 'https', auth: ':password' }, 278 | 'https://:password@gist.github.com/feedbeef#branch': { ...defaults, default: 'https', auth: ':password', committish: 'branch' }, 279 | 280 | 'https://gist.github.com/feedbeef.git': { ...defaults, default: 'https' }, 281 | 'https://gist.github.com/feedbeef.git#branch': { ...defaults, default: 'https', committish: 'branch' }, 282 | 'https://user@gist.github.com/feedbeef.git': { ...defaults, default: 'https', auth: 'user' }, 283 | 'https://user@gist.github.com/feedbeef.git#branch': { ...defaults, default: 'https', auth: 'user', committish: 'branch' }, 284 | 'https://user:password@gist.github.com/feedbeef.git': { ...defaults, default: 'https', auth: 'user:password' }, 285 | 'https://user:password@gist.github.com/feedbeef.git#branch': { ...defaults, default: 'https', auth: 'user:password', committish: 'branch' }, 286 | 'https://:password@gist.github.com/feedbeef.git': { ...defaults, default: 'https', auth: ':password' }, 287 | 'https://:password@gist.github.com/feedbeef.git#branch': { ...defaults, default: 'https', auth: ':password', committish: 'branch' }, 288 | 289 | 'https://gist.github.com/foo/feedbeef': { ...defaults, default: 'https', user: 'foo' }, 290 | 'https://gist.github.com/foo/feedbeef#branch': { ...defaults, default: 'https', user: 'foo', committish: 'branch' }, 291 | 'https://user@gist.github.com/foo/feedbeef': { ...defaults, default: 'https', auth: 'user', user: 'foo' }, 292 | 'https://user@gist.github.com/foo/feedbeef#branch': { ...defaults, default: 'https', auth: 'user', user: 'foo', committish: 'branch' }, 293 | 'https://user:password@gist.github.com/foo/feedbeef': { ...defaults, default: 'https', auth: 'user:password', user: 'foo' }, 294 | 'https://user:password@gist.github.com/foo/feedbeef#branch': { ...defaults, default: 'https', auth: 'user:password', user: 'foo', committish: 'branch' }, 295 | 'https://:password@gist.github.com/foo/feedbeef': { ...defaults, default: 'https', auth: ':password', user: 'foo' }, 296 | 'https://:password@gist.github.com/foo/feedbeef#branch': { ...defaults, default: 'https', auth: ':password', user: 'foo', committish: 'branch' }, 297 | 298 | 'https://gist.github.com/foo/feedbeef.git': { ...defaults, default: 'https', user: 'foo' }, 299 | 'https://gist.github.com/foo/feedbeef.git#branch': { ...defaults, default: 'https', user: 'foo', committish: 'branch' }, 300 | 'https://user@gist.github.com/foo/feedbeef.git': { ...defaults, default: 'https', auth: 'user', user: 'foo' }, 301 | 'https://user@gist.github.com/foo/feedbeef.git#branch': { ...defaults, default: 'https', auth: 'user', user: 'foo', committish: 'branch' }, 302 | 'https://user:password@gist.github.com/foo/feedbeef.git': { ...defaults, default: 'https', auth: 'user:password', user: 'foo' }, 303 | 'https://user:password@gist.github.com/foo/feedbeef.git#branch': { ...defaults, default: 'https', auth: 'user:password', user: 'foo', committish: 'branch' }, 304 | 'https://:password@gist.github.com/foo/feedbeef.git': { ...defaults, default: 'https', auth: ':password', user: 'foo' }, 305 | 'https://:password@gist.github.com/foo/feedbeef.git#branch': { ...defaults, default: 'https', auth: ':password', user: 'foo', committish: 'branch' }, 306 | } 307 | 308 | t.test('valid urls parse properly', t => { 309 | t.plan(Object.keys(valid).length) 310 | for (const [url, result] of Object.entries(valid)) { 311 | t.hasStrict(HostedGit.fromUrl(url), result, `${url} parses`) 312 | } 313 | }) 314 | 315 | t.test('invalid urls return undefined', t => { 316 | t.plan(invalid.length) 317 | for (const url of invalid) { 318 | t.equal(HostedGit.fromUrl(url), undefined, `${url} returns undefined`) 319 | } 320 | }) 321 | 322 | t.test('toString respects defaults', t => { 323 | const sshurl = HostedGit.fromUrl('git+ssh://gist.github.com/foo/feedbeef') 324 | t.equal(sshurl.default, 'sshurl', 'got the right default') 325 | t.equal(sshurl.toString(), sshurl.sshurl(), 'toString calls sshurl') 326 | 327 | const https = HostedGit.fromUrl('https://gist.github.com/foo/feedbeef') 328 | t.equal(https.default, 'https', 'got the right default') 329 | t.equal(https.toString(), https.https(), 'toString calls https') 330 | 331 | const shortcut = HostedGit.fromUrl('gist:feedbeef') 332 | t.equal(shortcut.default, 'shortcut', 'got the right default') 333 | t.equal(shortcut.toString(), shortcut.shortcut(), 'toString calls shortcut') 334 | 335 | t.end() 336 | }) 337 | 338 | t.test('string methods populate correctly', t => { 339 | const parsed = HostedGit.fromUrl('git+ssh://gist.github.com/foo/feedbeef') 340 | t.equal(parsed.getDefaultRepresentation(), parsed.default) 341 | t.equal(parsed.hash(), '', 'hash() returns empty string when committish is unset') 342 | t.equal(parsed.ssh(), 'git@gist.github.com:feedbeef.git') 343 | t.equal(parsed.sshurl(), 'git+ssh://git@gist.github.com/feedbeef.git') 344 | t.equal(parsed.edit(), 'https://gist.github.com/foo/feedbeef/edit', 'gist link only redirects with a user') 345 | t.equal(parsed.edit('/lib/index.js'), 'https://gist.github.com/foo/feedbeef/edit', 'gist link only redirects with a user') 346 | t.equal(parsed.browse(), 'https://gist.github.com/feedbeef') 347 | t.equal(parsed.browse('/lib/index.js'), 'https://gist.github.com/feedbeef#file-libindex-js') 348 | t.equal(parsed.browse('/lib/index.js', 'L100'), 'https://gist.github.com/feedbeef#file-libindex-js') 349 | t.equal(parsed.browseFile('/lib/index.js'), 'https://gist.github.com/feedbeef#file-libindex-js') 350 | t.equal(parsed.browseFile('/lib/index.js', 'L100'), 'https://gist.github.com/feedbeef#file-libindex-js') 351 | t.equal(parsed.docs(), 'https://gist.github.com/feedbeef') 352 | t.equal(parsed.https(), 'git+https://gist.github.com/feedbeef.git') 353 | t.equal(parsed.shortcut(), 'gist:feedbeef') 354 | t.equal(parsed.path(), 'feedbeef') 355 | t.equal(parsed.tarball(), 'https://codeload.github.com/gist/feedbeef/tar.gz/HEAD') 356 | t.equal(parsed.file(), 'https://gist.githubusercontent.com/foo/feedbeef/raw/') 357 | t.equal(parsed.file('/lib/index.js'), 'https://gist.githubusercontent.com/foo/feedbeef/raw/lib/index.js') 358 | t.equal(parsed.git(), 'git://gist.github.com/feedbeef.git') 359 | t.equal(parsed.bugs(), 'https://gist.github.com/feedbeef') 360 | 361 | t.equal(parsed.ssh({ committish: 'fix/bug' }), 'git@gist.github.com:feedbeef.git#fix/bug', 'allows overriding options') 362 | 363 | const extra = HostedGit.fromUrl('https://user@gist.github.com/foo/feedbeef#fix/bug') 364 | t.equal(extra.hash(), '#fix/bug') 365 | t.equal(extra.https(), 'git+https://gist.github.com/feedbeef.git#fix/bug') 366 | t.equal(extra.shortcut(), 'gist:feedbeef#fix/bug') 367 | t.equal(extra.ssh(), 'git@gist.github.com:feedbeef.git#fix/bug') 368 | t.equal(extra.sshurl(), 'git+ssh://git@gist.github.com/feedbeef.git#fix/bug') 369 | t.equal(extra.browse(), 'https://gist.github.com/feedbeef/fix%2Fbug') 370 | t.equal(extra.browse('/lib/index.js'), 'https://gist.github.com/feedbeef/fix%2Fbug#file-libindex-js') 371 | t.equal(extra.browse('/lib/index.js', 'L200'), 'https://gist.github.com/feedbeef/fix%2Fbug#file-libindex-js') 372 | t.equal(extra.docs(), 'https://gist.github.com/feedbeef/fix%2Fbug') 373 | t.equal(extra.file(), 'https://gist.githubusercontent.com/foo/feedbeef/raw/fix%2Fbug/') 374 | t.equal(extra.file('/lib/index.js'), 'https://gist.githubusercontent.com/foo/feedbeef/raw/fix%2Fbug/lib/index.js') 375 | t.equal(extra.tarball(), 'https://codeload.github.com/gist/feedbeef/tar.gz/fix%2Fbug') 376 | 377 | t.equal(extra.sshurl({ noCommittish: true }), 'git+ssh://git@gist.github.com/feedbeef.git', 'noCommittish drops committish from urls') 378 | t.equal(extra.sshurl({ noGitPlus: true }), 'ssh://git@gist.github.com/feedbeef.git#fix/bug', 'noGitPlus drops git+ prefix from urls') 379 | 380 | t.end() 381 | }) 382 | -------------------------------------------------------------------------------- /test/github.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | const HostedGit = require('..') 3 | const t = require('tap') 4 | 5 | const invalid = [ 6 | // foo/bar shorthand but specifying auth 7 | 'user@foo/bar', 8 | 'user:password@foo/bar', 9 | ':password@foo/bar', 10 | // foo/bar shorthand but with a space in it 11 | 'foo/ bar', 12 | // string that ends with a slash, probably a directory 13 | 'foo/bar/', 14 | // git@github.com style, but omitting the username 15 | 'github.com:foo/bar', 16 | 'github.com/foo/bar', 17 | // invalid URI encoding 18 | 'github:foo%0N/bar', 19 | // missing path 20 | 'git+ssh://git@github.com:', 21 | // a deep url to something we don't know 22 | 'https://github.com/foo/bar/issues', 23 | ] 24 | 25 | const defaults = { type: 'github', user: 'foo', project: 'bar' } 26 | // This is a valid git branch name that contains other occurences of the characters we check 27 | // for to determine the committish in order to test that we parse those correctly 28 | const committishDefaults = { committish: 'lk/br@nch.t#st:^1.0.0-pre.4' } 29 | const valid = { 30 | // extreme shorthand 31 | // 32 | // NOTE these do not accept auth at all 33 | 'foo/bar': { ...defaults, default: 'shortcut' }, 34 | [`foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'shortcut', ...committishDefaults }, 35 | 36 | 'foo/bar.git': { ...defaults, default: 'shortcut' }, 37 | [`foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'shortcut', ...committishDefaults }, 38 | 39 | // shortcuts 40 | // 41 | // NOTE auth is accepted but ignored 42 | 'github:foo/bar': { ...defaults, default: 'shortcut' }, 43 | [`github:foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'shortcut', ...committishDefaults }, 44 | 'github:user@foo/bar': { ...defaults, default: 'shortcut', auth: null }, 45 | [`github:user@foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'shortcut', auth: null, ...committishDefaults }, 46 | 'github:user:password@foo/bar': { ...defaults, default: 'shortcut', auth: null }, 47 | [`github:user:password@foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'shortcut', auth: null, ...committishDefaults }, 48 | 'github::password@foo/bar': { ...defaults, default: 'shortcut', auth: null }, 49 | [`github::password@foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'shortcut', auth: null, ...committishDefaults }, 50 | 51 | 'github:foo/bar.git': { ...defaults, default: 'shortcut' }, 52 | [`github:foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'shortcut', ...committishDefaults }, 53 | 'github:user@foo/bar.git': { ...defaults, default: 'shortcut', auth: null }, 54 | [`github:user@foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'shortcut', auth: null, ...committishDefaults }, 55 | 'github:user:password@foo/bar.git': { ...defaults, default: 'shortcut', auth: null }, 56 | [`github:user:password@foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'shortcut', auth: null, ...committishDefaults }, 57 | 'github::password@foo/bar.git': { ...defaults, default: 'shortcut', auth: null }, 58 | [`github::password@foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'shortcut', auth: null, ...committishDefaults }, 59 | 60 | // git urls 61 | // 62 | // NOTE auth is accepted and respected 63 | 'git://github.com/foo/bar': { ...defaults, default: 'git' }, 64 | [`git://github.com/foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'git', ...committishDefaults }, 65 | 'git://user@github.com/foo/bar': { ...defaults, default: 'git', auth: 'user' }, 66 | [`git://user@github.com/foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'git', auth: 'user', ...committishDefaults }, 67 | 'git://user:password@github.com/foo/bar': { ...defaults, default: 'git', auth: 'user:password' }, 68 | [`git://user:password@github.com/foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'git', auth: 'user:password', ...committishDefaults }, 69 | 'git://:password@github.com/foo/bar': { ...defaults, default: 'git', auth: ':password' }, 70 | [`git://:password@github.com/foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'git', auth: ':password', ...committishDefaults }, 71 | 72 | 'git://github.com/foo/bar.git': { ...defaults, default: 'git' }, 73 | [`git://github.com/foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'git', ...committishDefaults }, 74 | 'git://git@github.com/foo/bar.git': { ...defaults, default: 'git', auth: 'git' }, 75 | [`git://git@github.com/foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'git', auth: 'git', ...committishDefaults }, 76 | 'git://user:password@github.com/foo/bar.git': { ...defaults, default: 'git', auth: 'user:password' }, 77 | [`git://user:password@github.com/foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'git', auth: 'user:password', ...committishDefaults }, 78 | 'git://:password@github.com/foo/bar.git': { ...defaults, default: 'git', auth: ':password' }, 79 | [`git://:password@github.com/foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'git', auth: ':password', ...committishDefaults }, 80 | 81 | // no-protocol git+ssh 82 | // 83 | // NOTE auth is _required_ (see invalid list) but ignored 84 | 'user@github.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 85 | [`user@github.com:foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 86 | 'user:password@github.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 87 | [`user:password@github.com:foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 88 | ':password@github.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 89 | [`:password@github.com:foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 90 | 91 | 'user@github.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 92 | [`user@github.com:foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 93 | 'user:password@github.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 94 | [`user:password@github.com:foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 95 | ':password@github.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 96 | [`:password@github.com:foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 97 | 98 | // git+ssh urls 99 | // 100 | // NOTE auth is accepted but ignored 101 | 'git+ssh://github.com:foo/bar': { ...defaults, default: 'sshurl' }, 102 | [`git+ssh://github.com:foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', ...committishDefaults }, 103 | 'git+ssh://user@github.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 104 | [`git+ssh://user@github.com:foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 105 | 'git+ssh://user:password@github.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 106 | [`git+ssh://user:password@github.com:foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 107 | 'git+ssh://:password@github.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 108 | [`git+ssh://:password@github.com:foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 109 | 110 | 'git+ssh://github.com:foo/bar.git': { ...defaults, default: 'sshurl' }, 111 | [`git+ssh://github.com:foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', ...committishDefaults }, 112 | 'git+ssh://user@github.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 113 | [`git+ssh://user@github.com:foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 114 | 'git+ssh://user:password@github.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 115 | [`git+ssh://user:password@github.com:foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 116 | 'git+ssh://:password@github.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 117 | [`git+ssh://:password@github.com:foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 118 | 119 | // ssh urls 120 | // 121 | // NOTE auth is accepted but ignored 122 | 'ssh://github.com:foo/bar': { ...defaults, default: 'sshurl' }, 123 | [`ssh://github.com:foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', ...committishDefaults }, 124 | 'ssh://user@github.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 125 | [`ssh://user@github.com:foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 126 | 'ssh://user:password@github.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 127 | [`ssh://user:password@github.com:foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 128 | 'ssh://:password@github.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 129 | [`ssh://:password@github.com:foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 130 | 131 | 'ssh://github.com:foo/bar.git': { ...defaults, default: 'sshurl' }, 132 | [`ssh://github.com:foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', ...committishDefaults }, 133 | 'ssh://user@github.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 134 | [`ssh://user@github.com:foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 135 | 'ssh://user:password@github.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 136 | [`ssh://user:password@github.com:foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 137 | 'ssh://:password@github.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 138 | [`ssh://:password@github.com:foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'sshurl', auth: null, ...committishDefaults }, 139 | 140 | // git+https urls 141 | // 142 | // NOTE auth is accepted and respected 143 | 'git+https://github.com/foo/bar': { ...defaults, default: 'https' }, 144 | [`git+https://github.com/foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'https', ...committishDefaults }, 145 | 'git+https://user@github.com/foo/bar': { ...defaults, default: 'https', auth: 'user' }, 146 | [`git+https://user@github.com/foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'https', auth: 'user', ...committishDefaults }, 147 | 'git+https://user:password@github.com/foo/bar': { ...defaults, default: 'https', auth: 'user:password' }, 148 | [`git+https://user:password@github.com/foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'https', auth: 'user:password', ...committishDefaults }, 149 | 'git+https://:password@github.com/foo/bar': { ...defaults, default: 'https', auth: ':password' }, 150 | [`git+https://:password@github.com/foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'https', auth: ':password', ...committishDefaults }, 151 | 152 | 'git+https://github.com/foo/bar.git': { ...defaults, default: 'https' }, 153 | [`git+https://github.com/foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'https', ...committishDefaults }, 154 | 'git+https://user@github.com/foo/bar.git': { ...defaults, default: 'https', auth: 'user' }, 155 | [`git+https://user@github.com/foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'https', auth: 'user', ...committishDefaults }, 156 | 'git+https://user:password@github.com/foo/bar.git': { ...defaults, default: 'https', auth: 'user:password' }, 157 | [`git+https://user:password@github.com/foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'https', auth: 'user:password', ...committishDefaults }, 158 | 'git+https://:password@github.com/foo/bar.git': { ...defaults, default: 'https', auth: ':password' }, 159 | [`git+https://:password@github.com/foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'https', auth: ':password', ...committishDefaults }, 160 | 161 | // https urls 162 | // 163 | // NOTE auth is accepted and respected 164 | 'https://github.com/foo/bar': { ...defaults, default: 'https' }, 165 | [`https://github.com/foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'https', ...committishDefaults }, 166 | 'https://user@github.com/foo/bar': { ...defaults, default: 'https', auth: 'user' }, 167 | [`https://user@github.com/foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'https', auth: 'user', ...committishDefaults }, 168 | 'https://user:password@github.com/foo/bar': { ...defaults, default: 'https', auth: 'user:password' }, 169 | [`https://user:password@github.com/foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'https', auth: 'user:password', ...committishDefaults }, 170 | 'https://:password@github.com/foo/bar': { ...defaults, default: 'https', auth: ':password' }, 171 | [`https://:password@github.com/foo/bar#${committishDefaults.committish}`]: { ...defaults, default: 'https', auth: ':password', ...committishDefaults }, 172 | 173 | 'https://github.com/foo/bar.git': { ...defaults, default: 'https' }, 174 | [`https://github.com/foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'https', ...committishDefaults }, 175 | 'https://user@github.com/foo/bar.git': { ...defaults, default: 'https', auth: 'user' }, 176 | [`https://user@github.com/foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'https', auth: 'user', ...committishDefaults }, 177 | 'https://user:password@github.com/foo/bar.git': { ...defaults, default: 'https', auth: 'user:password' }, 178 | [`https://user:password@github.com/foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'https', auth: 'user:password', ...committishDefaults }, 179 | 'https://:password@github.com/foo/bar.git': { ...defaults, default: 'https', auth: ':password' }, 180 | [`https://:password@github.com/foo/bar.git#${committishDefaults.committish}`]: { ...defaults, default: 'https', auth: ':password', ...committishDefaults }, 181 | 182 | // inputs that are not quite proper but we accept anyway 183 | 'https://www.github.com/foo/bar': { ...defaults, default: 'https' }, 184 | 'foo/bar#branch with space': { ...defaults, default: 'shortcut', committish: 'branch with space' }, 185 | 'foo/bar#branch:with:colons': { ...defaults, default: 'shortcut', committish: 'branch:with:colons' }, 186 | 'https://github.com/foo/bar/tree/branch': { ...defaults, default: 'https', committish: 'branch' }, 187 | 'user..blerg--/..foo-js# . . . . . some . tags / / /': { ...defaults, default: 'shortcut', user: 'user..blerg--', project: '..foo-js', committish: ' . . . . . some . tags / / /' }, 188 | } 189 | 190 | t.test('valid urls parse properly', t => { 191 | t.plan(Object.keys(valid).length) 192 | for (const [url, result] of Object.entries(valid)) { 193 | t.hasStrict(HostedGit.fromUrl(url), result, `${url} parses`) 194 | } 195 | }) 196 | 197 | t.test('invalid urls return undefined', t => { 198 | t.plan(invalid.length) 199 | for (const url of invalid) { 200 | t.equal(HostedGit.fromUrl(url), undefined, `${url} returns undefined`) 201 | } 202 | }) 203 | 204 | t.test('toString respects defaults', t => { 205 | const sshurl = HostedGit.fromUrl('git+ssh://github.com/foo/bar') 206 | t.equal(sshurl.default, 'sshurl', 'got the right default') 207 | t.equal(sshurl.toString(), sshurl.sshurl(), 'toString calls sshurl') 208 | 209 | const https = HostedGit.fromUrl('https://github.com/foo/bar') 210 | t.equal(https.default, 'https', 'got the right default') 211 | t.equal(https.toString(), https.https(), 'toString calls https') 212 | 213 | const http = HostedGit.fromUrl('http://github.com/foo/bar') 214 | t.equal(http.default, 'http', 'got the right default') 215 | t.equal(http.toString(), http.sshurl(), 'automatically upgrades toString to sshurl') 216 | 217 | const git = HostedGit.fromUrl('git://github.com/foo/bar') 218 | t.equal(git.default, 'git', 'got the right default') 219 | t.equal(git.toString(), git.git(), 'toString calls git') 220 | 221 | const shortcut = HostedGit.fromUrl('github:foo/bar') 222 | t.equal(shortcut.default, 'shortcut', 'got the right default') 223 | t.equal(shortcut.toString(), shortcut.shortcut(), 'got the right default') 224 | 225 | t.end() 226 | }) 227 | 228 | t.test('string methods populate correctly', t => { 229 | const parsed = HostedGit.fromUrl('git+ssh://github.com/foo/bar') 230 | t.equal(parsed.getDefaultRepresentation(), parsed.default) 231 | t.equal(parsed.hash(), '', 'hash() returns empty string when committish is unset') 232 | t.equal(parsed.ssh(), 'git@github.com:foo/bar.git') 233 | t.equal(parsed.sshurl(), 'git+ssh://git@github.com/foo/bar.git') 234 | t.equal(parsed.edit(), 'https://github.com/foo/bar') 235 | t.equal(parsed.edit('/lib/index.js'), 'https://github.com/foo/bar/edit/HEAD/lib/index.js') 236 | t.equal(parsed.edit('/lib/index.js', { committish: 'docs' }), 'https://github.com/foo/bar/edit/docs/lib/index.js') 237 | t.equal(parsed.browse(), 'https://github.com/foo/bar') 238 | t.equal(parsed.browse('/lib/index.js'), 'https://github.com/foo/bar/tree/HEAD/lib/index.js') 239 | t.equal(parsed.browse('/lib/index.js', 'L100'), 'https://github.com/foo/bar/tree/HEAD/lib/index.js#l100') 240 | t.equal(parsed.browseFile('/lib/index.js'), 'https://github.com/foo/bar/blob/HEAD/lib/index.js') 241 | t.equal(parsed.browseFile('/lib/index.js', 'L100'), 'https://github.com/foo/bar/blob/HEAD/lib/index.js#l100') 242 | t.equal(parsed.docs(), 'https://github.com/foo/bar#readme') 243 | t.equal(parsed.https(), 'git+https://github.com/foo/bar.git') 244 | t.equal(parsed.shortcut(), 'github:foo/bar') 245 | t.equal(parsed.path(), 'foo/bar') 246 | t.equal(parsed.tarball(), 'https://codeload.github.com/foo/bar/tar.gz/HEAD') 247 | t.equal(parsed.file(), 'https://raw.githubusercontent.com/foo/bar/HEAD/') 248 | t.equal(parsed.file('/lib/index.js'), 'https://raw.githubusercontent.com/foo/bar/HEAD/lib/index.js') 249 | t.equal(parsed.git(), 'git://github.com/foo/bar.git') 250 | t.equal(parsed.bugs(), 'https://github.com/foo/bar/issues') 251 | 252 | t.equal(parsed.docs({ committish: 'fix/bug' }), 'https://github.com/foo/bar/tree/fix%2Fbug#readme', 'allows overriding options') 253 | 254 | const extra = HostedGit.fromUrl('https://user@github.com/foo/bar#fix/bug') 255 | t.equal(extra.hash(), '#fix/bug') 256 | t.equal(extra.https(), 'git+https://user@github.com/foo/bar.git#fix/bug') 257 | t.equal(extra.shortcut(), 'github:foo/bar#fix/bug') 258 | t.equal(extra.ssh(), 'git@github.com:foo/bar.git#fix/bug') 259 | t.equal(extra.sshurl(), 'git+ssh://git@github.com/foo/bar.git#fix/bug') 260 | t.equal(extra.browse(), 'https://github.com/foo/bar/tree/fix%2Fbug') 261 | t.equal(extra.browse('/lib/index.js'), 'https://github.com/foo/bar/tree/fix%2Fbug/lib/index.js') 262 | t.equal(extra.browse('/lib/index.js', 'L200'), 'https://github.com/foo/bar/tree/fix%2Fbug/lib/index.js#l200') 263 | t.equal(extra.docs(), 'https://github.com/foo/bar/tree/fix%2Fbug#readme') 264 | t.equal(extra.file(), 'https://user@raw.githubusercontent.com/foo/bar/fix%2Fbug/') 265 | t.equal(extra.file('/lib/index.js'), 'https://user@raw.githubusercontent.com/foo/bar/fix%2Fbug/lib/index.js') 266 | t.equal(extra.tarball(), 'https://codeload.github.com/foo/bar/tar.gz/fix%2Fbug') 267 | 268 | t.equal(extra.sshurl({ noCommittish: true }), 'git+ssh://git@github.com/foo/bar.git', 'noCommittish drops committish from urls') 269 | t.equal(extra.sshurl({ noGitPlus: true }), 'ssh://git@github.com/foo/bar.git#fix/bug', 'noGitPlus drops git+ prefix from urls') 270 | 271 | t.end() 272 | }) 273 | 274 | t.test('from manifest', t => { 275 | t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined') 276 | t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined') 277 | t.equal(HostedGit.fromManifest(false), undefined, 'false manifest returns undefined') 278 | t.equal(HostedGit.fromManifest(() => {}), undefined, 'function manifest returns undefined') 279 | 280 | const unknownHostRepo = { 281 | name: 'foo', 282 | repository: { 283 | url: 'https://nope.com', 284 | }, 285 | } 286 | t.same(HostedGit.fromManifest(unknownHostRepo), 'https://nope.com/') 287 | 288 | const insecureUnknownHostRepo = { 289 | name: 'foo', 290 | repository: { 291 | url: 'http://nope.com', 292 | }, 293 | } 294 | t.same(HostedGit.fromManifest(insecureUnknownHostRepo), 'https://nope.com/') 295 | 296 | const insecureGitUnknownHostRepo = { 297 | name: 'foo', 298 | repository: { 299 | url: 'git+http://nope.com', 300 | }, 301 | } 302 | t.same(HostedGit.fromManifest(insecureGitUnknownHostRepo), 'http://nope.com') 303 | 304 | const badRepo = { 305 | name: 'foo', 306 | repository: { 307 | url: '#', 308 | }, 309 | } 310 | t.equal(HostedGit.fromManifest(badRepo), null) 311 | 312 | const manifest = { 313 | name: 'foo', 314 | repository: { 315 | type: 'git', 316 | url: 'git+ssh://github.com/foo/bar.git', 317 | }, 318 | } 319 | 320 | const parsed = HostedGit.fromManifest(manifest) 321 | t.same(parsed.browse(), 'https://github.com/foo/bar') 322 | 323 | const monorepo = { 324 | name: 'clowncar', 325 | repository: { 326 | type: 'git', 327 | url: 'git+ssh://github.com/foo/bar.git', 328 | directory: 'packages/foo', 329 | }, 330 | } 331 | 332 | const honk = HostedGit.fromManifest(monorepo) 333 | t.same(honk.browse(monorepo.repository.directory), 'https://github.com/foo/bar/tree/HEAD/packages/foo') 334 | 335 | const stringRepo = { 336 | name: 'foo', 337 | repository: 'git+ssh://github.com/foo/bar.git', 338 | } 339 | const stringRepoParsed = HostedGit.fromManifest(stringRepo) 340 | t.same(stringRepoParsed.browse(), 'https://github.com/foo/bar') 341 | 342 | const nonStringRepo = { 343 | name: 'foo', 344 | repository: 42, 345 | } 346 | t.throws(() => HostedGit.fromManifest(nonStringRepo)) 347 | 348 | t.end() 349 | }) 350 | -------------------------------------------------------------------------------- /test/gitlab.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | 'use strict' 3 | const HostedGit = require('..') 4 | const t = require('tap') 5 | 6 | const invalid = [ 7 | // gitlab urls can contain a /-/ segment, make sure we ignore those 8 | 'https://gitlab.com/foo/-/something', 9 | // missing project 10 | 'https://gitlab.com/foo', 11 | // tarball, this should not parse so that it can be used for pacote's remote fetcher 12 | 'https://gitlab.com/foo/bar/repository/archive.tar.gz', 13 | 'https://gitlab.com/foo/bar/repository/archive.tar.gz?ref=49b393e2ded775f2df36ef2ffcb61b0359c194c9', 14 | ] 15 | 16 | const defaults = { type: 'gitlab', user: 'foo', project: 'bar' } 17 | const subgroup = { type: 'gitlab', user: 'foo/bar', project: 'baz' } 18 | const valid = { 19 | // shortcuts 20 | // 21 | // NOTE auth is accepted but ignored 22 | // NOTE subgroups are respected, but the subgroup is treated as the project and the real project is lost 23 | 'gitlab:foo/bar': { ...defaults, default: 'shortcut' }, 24 | 'gitlab:foo/bar#branch': { ...defaults, default: 'shortcut', committish: 'branch' }, 25 | 'gitlab:user@foo/bar': { ...defaults, default: 'shortcut', auth: null }, 26 | 'gitlab:user@foo/bar#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 27 | 'gitlab:user:password@foo/bar': { ...defaults, default: 'shortcut', auth: null }, 28 | 'gitlab:user:password@foo/bar#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 29 | 'gitlab::password@foo/bar': { ...defaults, default: 'shortcut', auth: null }, 30 | 'gitlab::password@foo/bar#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 31 | 32 | 'gitlab:foo/bar.git': { ...defaults, default: 'shortcut' }, 33 | 'gitlab:foo/bar.git#branch': { ...defaults, default: 'shortcut', committish: 'branch' }, 34 | 'gitlab:user@foo/bar.git': { ...defaults, default: 'shortcut', auth: null }, 35 | 'gitlab:user@foo/bar.git#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 36 | 'gitlab:user:password@foo/bar.git': { ...defaults, default: 'shortcut', auth: null }, 37 | 'gitlab:user:password@foo/bar.git#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 38 | 'gitlab::password@foo/bar.git': { ...defaults, default: 'shortcut', auth: null }, 39 | 'gitlab::password@foo/bar.git#branch': { ...defaults, default: 'shortcut', auth: null, committish: 'branch' }, 40 | 41 | 'gitlab:foo/bar/baz': { ...subgroup, default: 'shortcut' }, 42 | 'gitlab:foo/bar/baz#branch': { ...subgroup, default: 'shortcut', committish: 'branch' }, 43 | 'gitlab:user@foo/bar/baz': { ...subgroup, default: 'shortcut', auth: null }, 44 | 'gitlab:user@foo/bar/baz#branch': { ...subgroup, default: 'shortcut', auth: null, committish: 'branch' }, 45 | 'gitlab:user:password@foo/bar/baz': { ...subgroup, default: 'shortcut', auth: null }, 46 | 'gitlab:user:password@foo/bar/baz#branch': { ...subgroup, default: 'shortcut', auth: null, committish: 'branch' }, 47 | 'gitlab::password@foo/bar/baz': { ...subgroup, default: 'shortcut', auth: null }, 48 | 'gitlab::password@foo/bar/baz#branch': { ...subgroup, default: 'shortcut', auth: null, committish: 'branch' }, 49 | 50 | 'gitlab:foo/bar/baz.git': { ...subgroup, default: 'shortcut' }, 51 | 'gitlab:foo/bar/baz.git#branch': { ...subgroup, default: 'shortcut', committish: 'branch' }, 52 | 'gitlab:user@foo/bar/baz.git': { ...subgroup, default: 'shortcut', auth: null }, 53 | 'gitlab:user@foo/bar/baz.git#branch': { ...subgroup, default: 'shortcut', auth: null, committish: 'branch' }, 54 | 'gitlab:user:password@foo/bar/baz.git': { ...subgroup, default: 'shortcut', auth: null }, 55 | 'gitlab:user:password@foo/bar/baz.git#branch': { ...subgroup, default: 'shortcut', auth: null, committish: 'branch' }, 56 | 'gitlab::password@foo/bar/baz.git': { ...subgroup, default: 'shortcut', auth: null }, 57 | 'gitlab::password@foo/bar/baz.git#branch': { ...subgroup, default: 'shortcut', auth: null, committish: 'branch' }, 58 | 59 | // no-protocol git+ssh 60 | // 61 | // NOTE auth is _required_ (see invalid list) but ignored 62 | 'user@gitlab.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 63 | 'user@gitlab.com:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 64 | 'user:password@gitlab.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 65 | 'user:password@gitlab.com:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 66 | ':password@gitlab.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 67 | ':password@gitlab.com:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 68 | 69 | 'user@gitlab.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 70 | 'user@gitlab.com:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 71 | 'user:password@gitlab.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 72 | 'user:password@gitlab.com:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 73 | ':password@gitlab.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 74 | ':password@gitlab.com:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 75 | 76 | 'user@gitlab.com:foo/bar/baz': { ...subgroup, default: 'sshurl', auth: null }, 77 | 'user@gitlab.com:foo/bar/baz#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 78 | 'user:password@gitlab.com:foo/bar/baz': { ...subgroup, default: 'sshurl', auth: null }, 79 | 'user:password@gitlab.com:foo/bar/baz#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 80 | ':password@gitlab.com:foo/bar/baz': { ...subgroup, default: 'sshurl', auth: null }, 81 | ':password@gitlab.com:foo/bar/baz#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 82 | 83 | 'user@gitlab.com:foo/bar/baz.git': { ...subgroup, default: 'sshurl', auth: null }, 84 | 'user@gitlab.com:foo/bar/baz.git#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 85 | 'user:password@gitlab.com:foo/bar/baz.git': { ...subgroup, default: 'sshurl', auth: null }, 86 | 'user:password@gitlab.com:foo/bar/baz.git#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 87 | ':password@gitlab.com:foo/bar/baz.git': { ...subgroup, default: 'sshurl', auth: null }, 88 | ':password@gitlab.com:foo/bar/baz.git#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 89 | 90 | // git+ssh urls 91 | // 92 | // NOTE auth is accepted but ignored 93 | // NOTE subprojects are accepted, but the subproject is treated as the project and the real project is lost 94 | 'git+ssh://gitlab.com:foo/bar': { ...defaults, default: 'sshurl' }, 95 | 'git+ssh://gitlab.com:foo/bar#branch': { ...defaults, default: 'sshurl', committish: 'branch' }, 96 | 'git+ssh://user@gitlab.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 97 | 'git+ssh://user@gitlab.com:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 98 | 'git+ssh://user:password@gitlab.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 99 | 'git+ssh://user:password@gitlab.com:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 100 | 'git+ssh://:password@gitlab.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 101 | 'git+ssh://:password@gitlab.com:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 102 | 103 | 'git+ssh://gitlab.com:foo/bar.git': { ...defaults, default: 'sshurl' }, 104 | 'git+ssh://gitlab.com:foo/bar.git#branch': { ...defaults, default: 'sshurl', committish: 'branch' }, 105 | 'git+ssh://user@gitlab.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 106 | 'git+ssh://user@gitlab.com:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 107 | 'git+ssh://user:password@gitlab.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 108 | 'git+ssh://user:password@gitlab.com:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 109 | 'git+ssh://:password@gitlab.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 110 | 'git+ssh://:password@gitlab.com:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 111 | 112 | 'git+ssh://gitlab.com:foo/bar/baz': { ...subgroup, default: 'sshurl' }, 113 | 'git+ssh://gitlab.com:foo/bar/baz#branch': { ...subgroup, default: 'sshurl', committish: 'branch' }, 114 | 'git+ssh://user@gitlab.com:foo/bar/baz': { ...subgroup, default: 'sshurl', auth: null }, 115 | 'git+ssh://user@gitlab.com:foo/bar/baz#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 116 | 'git+ssh://user:password@gitlab.com:foo/bar/baz': { ...subgroup, default: 'sshurl', auth: null }, 117 | 'git+ssh://user:password@gitlab.com:foo/bar/baz#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 118 | 'git+ssh://:password@gitlab.com:foo/bar/baz': { ...subgroup, default: 'sshurl', auth: null }, 119 | 'git+ssh://:password@gitlab.com:foo/bar/baz#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 120 | 121 | 'git+ssh://gitlab.com:foo/bar/baz.git': { ...subgroup, default: 'sshurl' }, 122 | 'git+ssh://gitlab.com:foo/bar/baz.git#branch': { ...subgroup, default: 'sshurl', committish: 'branch' }, 123 | 'git+ssh://user@gitlab.com:foo/bar/baz.git': { ...subgroup, default: 'sshurl', auth: null }, 124 | 'git+ssh://user@gitlab.com:foo/bar/baz.git#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 125 | 'git+ssh://user:password@gitlab.com:foo/bar/baz.git': { ...subgroup, default: 'sshurl', auth: null }, 126 | 'git+ssh://user:password@gitlab.com:foo/bar/baz.git#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 127 | 'git+ssh://:password@gitlab.com:foo/bar/baz.git': { ...subgroup, default: 'sshurl', auth: null }, 128 | 'git+ssh://:password@gitlab.com:foo/bar/baz.git#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 129 | 130 | // ssh urls 131 | // 132 | // NOTE auth is accepted but ignored 133 | // NOTE subprojects are accepted, but the subproject is treated as the project and the real project is lost 134 | 'ssh://gitlab.com:foo/bar': { ...defaults, default: 'sshurl' }, 135 | 'ssh://gitlab.com:foo/bar#branch': { ...defaults, default: 'sshurl', committish: 'branch' }, 136 | 'ssh://user@gitlab.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 137 | 'ssh://user@gitlab.com:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 138 | 'ssh://user:password@gitlab.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 139 | 'ssh://user:password@gitlab.com:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 140 | 'ssh://:password@gitlab.com:foo/bar': { ...defaults, default: 'sshurl', auth: null }, 141 | 'ssh://:password@gitlab.com:foo/bar#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 142 | 143 | 'ssh://gitlab.com:foo/bar.git': { ...defaults, default: 'sshurl' }, 144 | 'ssh://gitlab.com:foo/bar.git#branch': { ...defaults, default: 'sshurl', committish: 'branch' }, 145 | 'ssh://user@gitlab.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 146 | 'ssh://user@gitlab.com:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 147 | 'ssh://user:password@gitlab.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 148 | 'ssh://user:password@gitlab.com:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 149 | 'ssh://:password@gitlab.com:foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 150 | 'ssh://:password@gitlab.com:foo/bar.git#branch': { ...defaults, default: 'sshurl', auth: null, committish: 'branch' }, 151 | 152 | 'ssh://gitlab.com:foo/bar/baz': { ...subgroup, default: 'sshurl' }, 153 | 'ssh://gitlab.com:foo/bar/baz#branch': { ...subgroup, default: 'sshurl', committish: 'branch' }, 154 | 'ssh://user@gitlab.com:foo/bar/baz': { ...subgroup, default: 'sshurl', auth: null }, 155 | 'ssh://user@gitlab.com:foo/bar/baz#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 156 | 'ssh://user:password@gitlab.com:foo/bar/baz': { ...subgroup, default: 'sshurl', auth: null }, 157 | 'ssh://user:password@gitlab.com:foo/bar/baz#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 158 | 'ssh://:password@gitlab.com:foo/bar/baz': { ...subgroup, default: 'sshurl', auth: null }, 159 | 'ssh://:password@gitlab.com:foo/bar/baz#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 160 | 161 | 'ssh://gitlab.com:foo/bar/baz.git': { ...subgroup, default: 'sshurl' }, 162 | 'ssh://gitlab.com:foo/bar/baz.git#branch': { ...subgroup, default: 'sshurl', committish: 'branch' }, 163 | 'ssh://user@gitlab.com:foo/bar/baz.git': { ...subgroup, default: 'sshurl', auth: null }, 164 | 'ssh://user@gitlab.com:foo/bar/baz.git#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 165 | 'ssh://user:password@gitlab.com:foo/bar/baz.git': { ...subgroup, default: 'sshurl', auth: null }, 166 | 'ssh://user:password@gitlab.com:foo/bar/baz.git#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 167 | 'ssh://:password@gitlab.com:foo/bar/baz.git': { ...subgroup, default: 'sshurl', auth: null }, 168 | 'ssh://:password@gitlab.com:foo/bar/baz.git#branch': { ...subgroup, default: 'sshurl', auth: null, committish: 'branch' }, 169 | 170 | // git+https urls 171 | // 172 | // NOTE auth is accepted and respected 173 | // NOTE subprojects are accepted, but the subproject is treated as the project and the real project is lost 174 | 'git+https://gitlab.com/foo/bar': { ...defaults, default: 'https' }, 175 | 'git+https://gitlab.com/foo/bar#branch': { ...defaults, default: 'https', committish: 'branch' }, 176 | 'git+https://user@gitlab.com/foo/bar': { ...defaults, default: 'https', auth: 'user' }, 177 | 'git+https://user@gitlab.com/foo/bar#branch': { ...defaults, default: 'https', auth: 'user', committish: 'branch' }, 178 | 'git+https://user:password@gitlab.com/foo/bar': { ...defaults, default: 'https', auth: 'user:password' }, 179 | 'git+https://user:password@gitlab.com/foo/bar#branch': { ...defaults, default: 'https', auth: 'user:password', committish: 'branch' }, 180 | 'git+https://:password@gitlab.com/foo/bar': { ...defaults, default: 'https', auth: ':password' }, 181 | 'git+https://:password@gitlab.com/foo/bar#branch': { ...defaults, default: 'https', auth: ':password', committish: 'branch' }, 182 | 183 | 'git+https://gitlab.com/foo/bar.git': { ...defaults, default: 'https' }, 184 | 'git+https://gitlab.com/foo/bar.git#branch': { ...defaults, default: 'https', committish: 'branch' }, 185 | 'git+https://user@gitlab.com/foo/bar.git': { ...defaults, default: 'https', auth: 'user' }, 186 | 'git+https://user@gitlab.com/foo/bar.git#branch': { ...defaults, default: 'https', auth: 'user', committish: 'branch' }, 187 | 'git+https://user:password@gitlab.com/foo/bar.git': { ...defaults, default: 'https', auth: 'user:password' }, 188 | 'git+https://user:password@gitlab.com/foo/bar.git#branch': { ...defaults, default: 'https', auth: 'user:password', committish: 'branch' }, 189 | 'git+https://:password@gitlab.com/foo/bar.git': { ...defaults, default: 'https', auth: ':password' }, 190 | 'git+https://:password@gitlab.com/foo/bar.git#branch': { ...defaults, default: 'https', auth: ':password', committish: 'branch' }, 191 | 192 | 'git+https://gitlab.com/foo/bar/baz': { ...subgroup, default: 'https' }, 193 | 'git+https://gitlab.com/foo/bar/baz#branch': { ...subgroup, default: 'https', committish: 'branch' }, 194 | 'git+https://user@gitlab.com/foo/bar/baz': { ...subgroup, default: 'https', auth: 'user' }, 195 | 'git+https://user@gitlab.com/foo/bar/baz#branch': { ...subgroup, default: 'https', auth: 'user', committish: 'branch' }, 196 | 'git+https://user:password@gitlab.com/foo/bar/baz': { ...subgroup, default: 'https', auth: 'user:password' }, 197 | 'git+https://user:password@gitlab.com/foo/bar/baz#branch': { ...subgroup, default: 'https', auth: 'user:password', committish: 'branch' }, 198 | 'git+https://:password@gitlab.com/foo/bar/baz': { ...subgroup, default: 'https', auth: ':password' }, 199 | 'git+https://:password@gitlab.com/foo/bar/baz#branch': { ...subgroup, default: 'https', auth: ':password', committish: 'branch' }, 200 | 201 | 'git+https://gitlab.com/foo/bar/baz.git': { ...subgroup, default: 'https' }, 202 | 'git+https://gitlab.com/foo/bar/baz.git#branch': { ...subgroup, default: 'https', committish: 'branch' }, 203 | 'git+https://user@gitlab.com/foo/bar/baz.git': { ...subgroup, default: 'https', auth: 'user' }, 204 | 'git+https://user@gitlab.com/foo/bar/baz.git#branch': { ...subgroup, default: 'https', auth: 'user', committish: 'branch' }, 205 | 'git+https://user:password@gitlab.com/foo/bar/baz.git': { ...subgroup, default: 'https', auth: 'user:password' }, 206 | 'git+https://user:password@gitlab.com/foo/bar/baz.git#branch': { ...subgroup, default: 'https', auth: 'user:password', committish: 'branch' }, 207 | 'git+https://:password@gitlab.com/foo/bar/baz.git': { ...subgroup, default: 'https', auth: ':password' }, 208 | 'git+https://:password@gitlab.com/foo/bar/baz.git#branch': { ...subgroup, default: 'https', auth: ':password', committish: 'branch' }, 209 | 210 | // https urls 211 | // 212 | // NOTE auth is accepted and respected 213 | // NOTE subprojects are accepted, but the subproject is treated as the project and the real project is lost 214 | 'https://gitlab.com/foo/bar': { ...defaults, default: 'https' }, 215 | 'https://gitlab.com/foo/bar#branch': { ...defaults, default: 'https', committish: 'branch' }, 216 | 'https://user@gitlab.com/foo/bar': { ...defaults, default: 'https', auth: 'user' }, 217 | 'https://user@gitlab.com/foo/bar#branch': { ...defaults, default: 'https', auth: 'user', committish: 'branch' }, 218 | 'https://user:password@gitlab.com/foo/bar': { ...defaults, default: 'https', auth: 'user:password' }, 219 | 'https://user:password@gitlab.com/foo/bar#branch': { ...defaults, default: 'https', auth: 'user:password', committish: 'branch' }, 220 | 'https://:password@gitlab.com/foo/bar': { ...defaults, default: 'https', auth: ':password' }, 221 | 'https://:password@gitlab.com/foo/bar#branch': { ...defaults, default: 'https', auth: ':password', committish: 'branch' }, 222 | 223 | 'https://gitlab.com/foo/bar.git': { ...defaults, default: 'https' }, 224 | 'https://gitlab.com/foo/bar.git#branch': { ...defaults, default: 'https', committish: 'branch' }, 225 | 'https://user@gitlab.com/foo/bar.git': { ...defaults, default: 'https', auth: 'user' }, 226 | 'https://user@gitlab.com/foo/bar.git#branch': { ...defaults, default: 'https', auth: 'user', committish: 'branch' }, 227 | 'https://user:password@gitlab.com/foo/bar.git': { ...defaults, default: 'https', auth: 'user:password' }, 228 | 'https://user:password@gitlab.com/foo/bar.git#branch': { ...defaults, default: 'https', auth: 'user:password', committish: 'branch' }, 229 | 'https://:password@gitlab.com/foo/bar.git': { ...defaults, default: 'https', auth: ':password' }, 230 | 'https://:password@gitlab.com/foo/bar.git#branch': { ...defaults, default: 'https', auth: ':password', committish: 'branch' }, 231 | 232 | 'https://gitlab.com/foo/bar/baz': { ...subgroup, default: 'https' }, 233 | 'https://gitlab.com/foo/bar/baz#branch': { ...subgroup, default: 'https', committish: 'branch' }, 234 | 'https://user@gitlab.com/foo/bar/baz': { ...subgroup, default: 'https', auth: 'user' }, 235 | 'https://user@gitlab.com/foo/bar/baz#branch': { ...subgroup, default: 'https', auth: 'user', committish: 'branch' }, 236 | 'https://user:password@gitlab.com/foo/bar/baz': { ...subgroup, default: 'https', auth: 'user:password' }, 237 | 'https://user:password@gitlab.com/foo/bar/baz#branch': { ...subgroup, default: 'https', auth: 'user:password', committish: 'branch' }, 238 | 'https://:password@gitlab.com/foo/bar/baz': { ...subgroup, default: 'https', auth: ':password' }, 239 | 'https://:password@gitlab.com/foo/bar/baz#branch': { ...subgroup, default: 'https', auth: ':password', committish: 'branch' }, 240 | 241 | 'https://gitlab.com/foo/bar/baz.git': { ...subgroup, default: 'https' }, 242 | 'https://gitlab.com/foo/bar/baz.git#branch': { ...subgroup, default: 'https', committish: 'branch' }, 243 | 'https://user@gitlab.com/foo/bar/baz.git': { ...subgroup, default: 'https', auth: 'user' }, 244 | 'https://user@gitlab.com/foo/bar/baz.git#branch': { ...subgroup, default: 'https', auth: 'user', committish: 'branch' }, 245 | 'https://user:password@gitlab.com/foo/bar/baz.git': { ...subgroup, default: 'https', auth: 'user:password' }, 246 | 'https://user:password@gitlab.com/foo/bar/baz.git#branch': { ...subgroup, default: 'https', auth: 'user:password', committish: 'branch' }, 247 | 'https://:password@gitlab.com/foo/bar/baz.git': { ...subgroup, default: 'https', auth: ':password' }, 248 | 'https://:password@gitlab.com/foo/bar/baz.git#branch': { ...subgroup, default: 'https', auth: ':password', committish: 'branch' }, 249 | } 250 | 251 | t.test('valid urls parse properly', t => { 252 | t.plan(Object.keys(valid).length) 253 | for (const [url, result] of Object.entries(valid)) { 254 | t.hasStrict(HostedGit.fromUrl(url), result, `${url} parses`) 255 | } 256 | }) 257 | 258 | t.test('invalid urls return undefined', t => { 259 | t.plan(invalid.length) 260 | for (const url of invalid) { 261 | t.equal(HostedGit.fromUrl(url), undefined, `${url} returns undefined`) 262 | } 263 | }) 264 | 265 | t.test('toString respects defaults', t => { 266 | const sshurl = HostedGit.fromUrl('git+ssh://gitlab.com/foo/bar') 267 | t.equal(sshurl.default, 'sshurl', 'got the right default') 268 | t.equal(sshurl.toString(), sshurl.sshurl(), 'toString calls sshurl') 269 | 270 | const https = HostedGit.fromUrl('https://gitlab.com/foo/bar') 271 | t.equal(https.default, 'https', 'got the right default') 272 | t.equal(https.toString(), https.https(), 'toString calls https') 273 | 274 | const shortcut = HostedGit.fromUrl('gitlab:foo/bar') 275 | t.equal(shortcut.default, 'shortcut', 'got the right default') 276 | t.equal(shortcut.toString(), shortcut.shortcut(), 'toString calls shortcut') 277 | 278 | t.end() 279 | }) 280 | 281 | t.test('string methods populate correctly', t => { 282 | const parsed = HostedGit.fromUrl('git+ssh://gitlab.com/foo/bar') 283 | t.equal(parsed.getDefaultRepresentation(), parsed.default) 284 | t.equal(parsed.hash(), '', 'hash() returns empty string when committish is unset') 285 | t.equal(parsed.ssh(), 'git@gitlab.com:foo/bar.git') 286 | t.equal(parsed.sshurl(), 'git+ssh://git@gitlab.com/foo/bar.git') 287 | t.equal(parsed.edit(), 'https://gitlab.com/foo/bar') 288 | t.equal(parsed.edit('/lib/index.js'), 'https://gitlab.com/foo/bar/-/edit/HEAD/lib/index.js') 289 | t.equal(parsed.browse(), 'https://gitlab.com/foo/bar') 290 | t.equal(parsed.browse('/lib/index.js'), 'https://gitlab.com/foo/bar/tree/HEAD/lib/index.js') 291 | t.equal(parsed.browse('/lib/index.js', 'L100'), 'https://gitlab.com/foo/bar/tree/HEAD/lib/index.js#l100') 292 | t.equal(parsed.docs(), 'https://gitlab.com/foo/bar#readme') 293 | t.equal(parsed.https(), 'git+https://gitlab.com/foo/bar.git') 294 | t.equal(parsed.shortcut(), 'gitlab:foo/bar') 295 | t.equal(parsed.path(), 'foo/bar') 296 | t.equal(parsed.tarball(), 'https://gitlab.com/foo/bar/repository/archive.tar.gz?ref=HEAD') 297 | t.equal(parsed.file(), 'https://gitlab.com/foo/bar/raw/HEAD/') 298 | t.equal(parsed.file('/lib/index.js'), 'https://gitlab.com/foo/bar/raw/HEAD/lib/index.js') 299 | t.equal(parsed.bugs(), 'https://gitlab.com/foo/bar/issues') 300 | 301 | t.same(parsed.git(), null, 'git() returns null') 302 | 303 | t.equal(parsed.docs({ committish: 'fix/bug' }), 'https://gitlab.com/foo/bar/tree/fix%2Fbug#readme', 'allows overriding options') 304 | 305 | const extra = HostedGit.fromUrl('https://user@gitlab.com/foo/bar#fix/bug') 306 | t.equal(extra.hash(), '#fix/bug') 307 | t.equal(extra.https(), 'git+https://user@gitlab.com/foo/bar.git#fix/bug') 308 | t.equal(extra.shortcut(), 'gitlab:foo/bar#fix/bug') 309 | t.equal(extra.ssh(), 'git@gitlab.com:foo/bar.git#fix/bug') 310 | t.equal(extra.sshurl(), 'git+ssh://git@gitlab.com/foo/bar.git#fix/bug') 311 | t.equal(extra.browse(), 'https://gitlab.com/foo/bar/tree/fix%2Fbug') 312 | t.equal(extra.browse('/lib/index.js'), 'https://gitlab.com/foo/bar/tree/fix%2Fbug/lib/index.js') 313 | t.equal(extra.browse('/lib/index.js', 'L200'), 'https://gitlab.com/foo/bar/tree/fix%2Fbug/lib/index.js#l200') 314 | t.equal(extra.docs(), 'https://gitlab.com/foo/bar/tree/fix%2Fbug#readme') 315 | t.equal(extra.file(), 'https://gitlab.com/foo/bar/raw/fix%2Fbug/') 316 | t.equal(extra.file('/lib/index.js'), 'https://gitlab.com/foo/bar/raw/fix%2Fbug/lib/index.js') 317 | t.equal(extra.tarball(), 'https://gitlab.com/foo/bar/repository/archive.tar.gz?ref=fix%2Fbug') 318 | 319 | t.equal(extra.sshurl({ noCommittish: true }), 'git+ssh://git@gitlab.com/foo/bar.git', 'noCommittish drops committish from urls') 320 | t.equal(extra.sshurl({ noGitPlus: true }), 'ssh://git@gitlab.com/foo/bar.git#fix/bug', 'noGitPlus drops git+ prefix from urls') 321 | 322 | t.end() 323 | }) 324 | 325 | t.test('from manifest', t => { 326 | t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined') 327 | t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined') 328 | t.equal(HostedGit.fromManifest(false), undefined, 'false manifest returns undefined') 329 | t.equal(HostedGit.fromManifest(() => {}), undefined, 'function manifest returns undefined') 330 | 331 | const unknownHostRepo = { 332 | name: 'foo', 333 | repository: { 334 | url: 'https://nope.com', 335 | }, 336 | } 337 | t.same(HostedGit.fromManifest(unknownHostRepo), 'https://nope.com/') 338 | 339 | const insecureUnknownHostRepo = { 340 | name: 'foo', 341 | repository: { 342 | url: 'http://nope.com', 343 | }, 344 | } 345 | t.same(HostedGit.fromManifest(insecureUnknownHostRepo), 'https://nope.com/') 346 | 347 | const insecureGitUnknownHostRepo = { 348 | name: 'foo', 349 | repository: { 350 | url: 'git+http://nope.com', 351 | }, 352 | } 353 | t.same(HostedGit.fromManifest(insecureGitUnknownHostRepo), 'http://nope.com') 354 | 355 | const badRepo = { 356 | name: 'foo', 357 | repository: { 358 | url: '#', 359 | }, 360 | } 361 | t.equal(HostedGit.fromManifest(badRepo), null) 362 | 363 | const manifest = { 364 | name: 'foo', 365 | repository: { 366 | type: 'git', 367 | url: 'git+ssh://gitlab.com/foo/bar.git', 368 | }, 369 | } 370 | 371 | const parsed = HostedGit.fromManifest(manifest) 372 | t.same(parsed.browse(), 'https://gitlab.com/foo/bar') 373 | 374 | const monorepo = { 375 | name: 'clowncar', 376 | repository: { 377 | type: 'git', 378 | url: 'git+ssh://gitlab.com/foo/bar.git', 379 | directory: 'packages/foo', 380 | }, 381 | } 382 | 383 | const honk = HostedGit.fromManifest(monorepo) 384 | t.same(honk.browse(monorepo.repository.directory), 'https://gitlab.com/foo/bar/tree/HEAD/packages/foo') 385 | 386 | const stringRepo = { 387 | name: 'foo', 388 | repository: 'git+ssh://gitlab.com/foo/bar.git', 389 | } 390 | const stringRepoParsed = HostedGit.fromManifest(stringRepo) 391 | t.same(stringRepoParsed.browse(), 'https://gitlab.com/foo/bar') 392 | 393 | const nonStringRepo = { 394 | name: 'foo', 395 | repository: 42, 396 | } 397 | t.throws(() => HostedGit.fromManifest(nonStringRepo)) 398 | 399 | t.end() 400 | }) 401 | -------------------------------------------------------------------------------- /test/invalid.js: -------------------------------------------------------------------------------- 1 | const HostedGit = require('..') 2 | const t = require('tap') 3 | 4 | // each of these urls should return `undefined` 5 | // none should throw 6 | const urls = [ 7 | 'https://google.com', 8 | 'git+ssh://git@nothosted.com/abc/def', 9 | 'git://nothosted.com', 10 | 'git+file:///foo/bar', 11 | 'git+ssh://git@git.unlucky.com:RND/electron-tools/some-tool#2.0.1', 12 | '::', 13 | '', 14 | null, 15 | undefined, 16 | ] 17 | 18 | t.test('invalid results parse to undefined', t => { 19 | t.plan(urls.length) 20 | for (const url of urls) { 21 | t.equal(HostedGit.fromUrl(url), undefined, `${url} returns undefined`) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /test/localhost.js: -------------------------------------------------------------------------------- 1 | const HostedGit = require('..') 2 | const t = require('tap') 3 | 4 | t.test('supports extensions', t => { 5 | // An example of a custom setup, useful when testing modules like pacote, 6 | // which do various things with these git shortcuts. 7 | HostedGit.addHost('localhost', { 8 | protocols: ['git:'], 9 | domain: 'localhost', 10 | extract: (url) => { 11 | const [, user, project] = url.pathname.split('/') 12 | return { user, project, committish: url.hash.slice(1) } 13 | }, 14 | }) 15 | 16 | const hosted = HostedGit.fromUrl('git://localhost:12345/foo/bar') 17 | t.match( 18 | hosted, 19 | { type: 'localhost', default: 'git', user: 'foo', project: 'bar' }, 20 | 'parsed correctly' 21 | ) 22 | 23 | const shortcut = HostedGit.fromUrl('localhost:foo/bar') 24 | t.match( 25 | shortcut, 26 | { type: 'localhost', default: 'shortcut', user: 'foo', project: 'bar' }, 27 | 'parsed correctly' 28 | ) 29 | 30 | t.end() 31 | }) 32 | -------------------------------------------------------------------------------- /test/parse-url.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const HostedGit = require('..') 3 | const parseUrl = require('../lib/parse-url.js') 4 | 5 | t.test('can parse git+ssh urls', async t => { 6 | // https://github.com/npm/cli/issues/5278 7 | const u = 'git+ssh://git@abc:frontend/utils.git#6d45447e0c5eb6cd2e3edf05a8c5a9bb81950c79' 8 | t.ok(parseUrl(u)) 9 | t.ok(HostedGit.parseUrl(u)) 10 | }) 11 | 12 | t.test('can parse file urls', async t => { 13 | // https://github.com/npm/cli/pull/5758#issuecomment-1292753331 14 | const u = 'file:../../../global-prefix/lib/node_modules/@myscope/bar' 15 | t.ok(parseUrl(u)) 16 | t.ok(HostedGit.parseUrl(u)) 17 | }) 18 | -------------------------------------------------------------------------------- /test/sourcehut.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const HostedGit = require('..') 3 | const t = require('tap') 4 | 5 | const invalid = [ 6 | // missing project 7 | 'https://git.sr.ht/~foo', 8 | // invalid protocos 9 | 'git://git@git.sr.ht:~foo/bar', 10 | 'ssh://git.sr.ht:~foo/bar', 11 | // tarball url 12 | 'https://git.sr.ht/~foo/bar/archive/HEAD.tar.gz', 13 | ] 14 | 15 | const defaults = { type: 'sourcehut', user: '~foo', project: 'bar' } 16 | 17 | const valid = { 18 | // shortucts 19 | 'sourcehut:~foo/bar': { ...defaults, default: 'shortcut' }, 20 | 'sourcehut:~foo/bar#branch': { ...defaults, default: 'shortcut', committish: 'branch' }, 21 | 22 | // shortcuts (.git) 23 | 'sourcehut:~foo/bar.git': { ...defaults, default: 'shortcut' }, 24 | 'sourcehut:~foo/bar.git#branch': { ...defaults, default: 'shortcut', committish: 'branch' }, 25 | 26 | // no-protocol git+ssh 27 | 'git@git.sr.ht:~foo/bar': { ...defaults, default: 'sshurl', auth: null }, 28 | 'git@git.sr.ht:~foo/bar#branch': { 29 | ...defaults, default: 'sshurl', auth: null, committish: 'branch', 30 | }, 31 | 32 | // no-protocol git+ssh (.git) 33 | 'git@git.sr.ht:~foo/bar.git': { ...defaults, default: 'sshurl', auth: null }, 34 | 'git@git.sr.ht:~foo/bar.git#branch': { 35 | ...defaults, default: 'sshurl', auth: null, committish: 'branch', 36 | }, 37 | 38 | // git+ssh urls 39 | 'git+ssh://git@git.sr.ht:~foo/bar': { ...defaults, default: 'sshurl' }, 40 | 'git+ssh://git@git.sr.ht:~foo/bar#branch': { 41 | ...defaults, default: 'sshurl', committish: 'branch', 42 | }, 43 | 44 | // git+ssh urls (.git) 45 | 'git+ssh://git@git.sr.ht:~foo/bar.git': { ...defaults, default: 'sshurl' }, 46 | 'git+ssh://git@git.sr.ht:~foo/bar.git#branch': { 47 | ...defaults, default: 'sshurl', committish: 'branch', 48 | }, 49 | 50 | // https urls 51 | 'https://git.sr.ht/~foo/bar': { ...defaults, default: 'https' }, 52 | 'https://git.sr.ht/~foo/bar#branch': { ...defaults, default: 'https', committish: 'branch' }, 53 | 54 | 'https://git.sr.ht/~foo/bar.git': { ...defaults, default: 'https' }, 55 | 'https://git.sr.ht/~foo/bar.git#branch': { ...defaults, default: 'https', committish: 'branch' }, 56 | } 57 | 58 | t.test('valid urls parse properly', t => { 59 | t.plan(Object.keys(valid).length) 60 | for (const [url, result] of Object.entries(valid)) { 61 | t.hasStrict(HostedGit.fromUrl(url), result, `${url} parses`) 62 | } 63 | }) 64 | 65 | t.test('invalid urls return undefined', t => { 66 | t.plan(invalid.length) 67 | for (const url of invalid) { 68 | t.equal(HostedGit.fromUrl(url), undefined, `${url} returns undefined`) 69 | } 70 | }) 71 | 72 | t.test('toString respects defaults', t => { 73 | const sshurl = HostedGit.fromUrl('git+ssh://git.sr.ht/~foo/bar') 74 | t.equal(sshurl.default, 'sshurl', 'got the right default') 75 | t.equal(sshurl.toString(), sshurl.sshurl(), 'toString calls sshurl') 76 | 77 | const https = HostedGit.fromUrl('https://git.sr.ht/~foo/bar') 78 | t.equal(https.default, 'https', 'got the right default') 79 | t.equal(https.toString(), https.https(), 'toString calls https') 80 | 81 | const shortcut = HostedGit.fromUrl('sourcehut:~foo/bar') 82 | t.equal(shortcut.default, 'shortcut', 'got the right default') 83 | t.equal(shortcut.toString(), shortcut.shortcut(), 'toString calls shortcut') 84 | 85 | t.end() 86 | }) 87 | 88 | t.test('string methods populate correctly', t => { 89 | const parsed = HostedGit.fromUrl('git+ssh://git.sr.ht/~foo/bar') 90 | t.equal(parsed.getDefaultRepresentation(), parsed.default, 'getDefaultRepresentation()') 91 | t.equal(parsed.hash(), '', 'hash() returns empty string when committish is unset') 92 | t.equal(parsed.ssh(), 'git@git.sr.ht:~foo/bar.git') 93 | t.equal(parsed.sshurl(), 'git+ssh://git@git.sr.ht/~foo/bar.git') 94 | t.equal(parsed.edit('/lib/index.js'), 'https://git.sr.ht/~foo/bar', 'no editing, link to browse') 95 | t.equal(parsed.edit(), 'https://git.sr.ht/~foo/bar', 'no editing, link to browse') 96 | t.equal(parsed.browse(), 'https://git.sr.ht/~foo/bar') 97 | t.equal(parsed.browse('/lib/index.js'), 'https://git.sr.ht/~foo/bar/tree/HEAD/lib/index.js') 98 | t.equal( 99 | parsed.browse('/lib/index.js', 'L100'), 100 | 'https://git.sr.ht/~foo/bar/tree/HEAD/lib/index.js#l100' 101 | ) 102 | t.equal(parsed.docs(), 'https://git.sr.ht/~foo/bar#readme') 103 | t.equal(parsed.https(), 'https://git.sr.ht/~foo/bar.git') 104 | t.equal(parsed.shortcut(), 'sourcehut:~foo/bar') 105 | t.equal(parsed.path(), '~foo/bar') 106 | t.equal(parsed.tarball(), 'https://git.sr.ht/~foo/bar/archive/HEAD.tar.gz') 107 | t.equal(parsed.file(), 'https://git.sr.ht/~foo/bar/blob/HEAD/') 108 | t.equal(parsed.file('/lib/index.js'), 'https://git.sr.ht/~foo/bar/blob/HEAD/lib/index.js') 109 | t.equal(parsed.bugs(), null) 110 | 111 | t.equal( 112 | parsed.docs({ committish: 'fix/bug' }), 113 | 'https://git.sr.ht/~foo/bar/tree/fix%2Fbug#readme', 114 | 'allows overriding options' 115 | ) 116 | 117 | t.same(parsed.git(), null, 'git() returns null') 118 | 119 | const extra = HostedGit.fromUrl('https://@git.sr.ht/~foo/bar#fix/bug') 120 | t.equal(extra.hash(), '#fix/bug') 121 | t.equal(extra.https(), 'https://git.sr.ht/~foo/bar.git#fix/bug') 122 | t.equal(extra.shortcut(), 'sourcehut:~foo/bar#fix/bug') 123 | t.equal(extra.ssh(), 'git@git.sr.ht:~foo/bar.git#fix/bug') 124 | t.equal(extra.sshurl(), 'git+ssh://git@git.sr.ht/~foo/bar.git#fix/bug') 125 | t.equal(extra.browse(), 'https://git.sr.ht/~foo/bar/tree/fix%2Fbug') 126 | t.equal(extra.browse('/lib/index.js'), 'https://git.sr.ht/~foo/bar/tree/fix%2Fbug/lib/index.js') 127 | t.equal( 128 | extra.browse('/lib/index.js', 'L200'), 129 | 'https://git.sr.ht/~foo/bar/tree/fix%2Fbug/lib/index.js#l200' 130 | ) 131 | t.equal(extra.docs(), 'https://git.sr.ht/~foo/bar/tree/fix%2Fbug#readme') 132 | t.equal(extra.file(), 'https://git.sr.ht/~foo/bar/blob/fix%2Fbug/') 133 | t.equal(extra.file('/lib/index.js'), 'https://git.sr.ht/~foo/bar/blob/fix%2Fbug/lib/index.js') 134 | 135 | t.equal( 136 | extra.sshurl({ noCommittish: true }), 137 | 'git+ssh://git@git.sr.ht/~foo/bar.git', 138 | 'noCommittish drops committish from urls' 139 | ) 140 | t.equal( 141 | extra.sshurl({ noGitPlus: true }), 142 | 'ssh://git@git.sr.ht/~foo/bar.git#fix/bug', 143 | 'noGitPlus drops git+ prefix from urls' 144 | ) 145 | 146 | t.end() 147 | }) 148 | --------------------------------------------------------------------------------