Repository: Rich-Harris/degit Branch: master Commit: 64b80577acf3 Files: 21 Total size: 38.6 KB Directory structure: gitextract_a5mbvtuo/ ├── .dependabot/ │ └── config.yml ├── .editorconfig ├── .eslintrc.json ├── .github/ │ └── workflows/ │ └── nodejs.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── appveyor.yml ├── degit ├── help.md ├── package.json ├── rollup.config.js ├── src/ │ ├── bin.js │ ├── index.js │ └── utils.js └── test/ └── test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dependabot/config.yml ================================================ version: 1 update_configs: - package_manager: 'javascript' directory: '/' update_schedule: 'weekly' target_branch: 'master' ================================================ FILE: .editorconfig ================================================ # editorconfig.org root = true [*] charset = utf-8 indent_size = 2 indent_style = tab end_of_line = lf trim_trailing_whitespace = true insert_final_newline = true [*.md] indent_style = tab trim_trailing_whitespace = false [{*.json,*.yml}] indent_style = tab ================================================ FILE: .eslintrc.json ================================================ { "root": true, "rules": { "no-cond-assign": 0, "no-unused-vars": 2, "object-shorthand": [2, "always"], "no-console": 0, "no-const-assign": 2, "no-class-assign": 2, "no-this-before-super": 2, "no-var": 2, "no-unreachable": 2, "valid-typeof": 2, "one-var": [2, "never"], "prefer-arrow-callback": 2, "prefer-const": [2, { "destructuring": "all" }], "no-inner-declarations": 0 }, "env": { "es6": true, "node": true, "mocha": true }, "extends": [ "eslint:recommended", "plugin:import/errors", "plugin:import/warnings", "prettier" ], "parserOptions": { "ecmaVersion": 8, "sourceType": "module" } } ================================================ FILE: .github/workflows/nodejs.yml ================================================ name: Node.js CI on: [push] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [8.x, 10.x, 12.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run build - run: npm test - run: npm run lint env: CI: true ================================================ FILE: .gitignore ================================================ .DS_Store node_modules .tmp dist ================================================ FILE: .npmrc ================================================ save-exact=true ================================================ FILE: .prettierrc ================================================ { "arrowParens": "avoid", "bracketSpacing": true, "endOfLine": "lf", "htmlWhitespaceSensitivity": "css", "insertPragma": false, "printWidth": 80, "proseWrap": "preserve", "requirePragma": false, "quoteProps": "as-needed", "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": false, "useTabs": true } ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - '8' - '10' - lts/* - node addons: apt: sources: - ubuntu-toolchain-r-test packages: - libstdc++-4.9-dev script: - npm run lint - npm test ================================================ FILE: CHANGELOG.md ================================================ # degit changelog ## 2.8.4 * Whoops ## 2.8.3 * Reinstate `#!/usr/bin/env node` ([#273](https://github.com/Rich-Harris/degit/issues/273)) ## 2.8.2 * Fix `bin`/`main` locations ([#273](https://github.com/Rich-Harris/degit/issues/273)) * Update dependencies ## 2.8.1 * Use `HEAD` instead of `master` ([#243](https://github.com/Rich-Harris/degit/pull/243)]) ## 2.8.0 * Sort by recency in interactive mode ## 2.7.0 * Bundle for a faster install ## 2.6.0 * Add an interactive mode ([#4](https://github.com/Rich-Harris/degit/issues/4)) ## 2.5.0 * Add `--mode=git` for cloning private repos ([#29](https://github.com/Rich-Harris/degit/pull/29)) ## 2.4.0 * Clone subdirectories from repos (`user/repo/subdir`) ## 2.3.0 * Support HTTPS proxying where `https_proxy` env var is supplied ([#26](https://github.com/Rich-Harris/degit/issues/26)) ## 2.2.2 - Improve CLI error logging ([#49](https://github.com/Rich-Harris/degit/pull/49)) ## 2.2.1 - Update `help.md` for Sourcehut support ## 2.2.0 - Sourcehut support ([#85](https://github.com/Rich-Harris/degit/pull/85)) ## 2.1.4 - Fix actions ([#65](https://github.com/Rich-Harris/degit/pull/65)) - Improve CLI error logging ([#46](https://github.com/Rich-Harris/degit/pull/46)) ## 2.1.3 - Install `sander` ([#34](https://github.com/Rich-Harris/degit/issues/34)) ## 2.1.2 - Remove `console.log` ## 2.1.1 - Oops, managed to publish 2.1.0 without building ## 2.1.0 - Add actions ([#28](https://github.com/Rich-Harris/degit/pull/28)) ## 2.0.2 - Allow flags like `-v` before argument ([#25](https://github.com/Rich-Harris/degit/issues/25)) ## 2.0.1 - Update node-tar for Node 9 compatibility ## 2.0.0 - Expose API for use in Node scripts ([#23](https://github.com/Rich-Harris/degit/issues/23)) ## 1.2.2 - Fix `files` in package.json ## 1.2.1 - Add `engines` field ([#17](https://github.com/Rich-Harris/degit/issues/17)) ## 1.2.0 - Windows support ([#1](https://github.com/Rich-Harris/degit/issues/1)) - Offline support and `--cache` flag ([#8](https://github.com/Rich-Harris/degit/issues/8)) - `degit --help` ([#5](https://github.com/Rich-Harris/degit/issues/5)) - `--verbose` flag ## 1.1.0 - Use HTTPS, not SSH ([#11](https://github.com/Rich-Harris/degit/issues/11)) ## 1.0.0 - First release ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at yogiboaron@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: LICENSE.md ================================================ Copyright (c) 2019 [these people](https://github.com/Rich-Harris/degit/graphs/contributors) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # degit — straightforward project scaffolding [![Travis CI build status](https://badgen.net/travis/Rich-Harris/degit/master)](https://travis-ci.org/Rich-Harris/degit) [![AppVeyor build status](https://badgen.net/appveyor/ci/Rich-Harris/degit/master)](https://ci.appveyor.com/project/Rich-Harris/degit/branch/master) [![Known Vulnerabilities](https://snyk.io/test/npm/degit/badge.svg)](https://snyk.io/test/npm/degit) [![install size](https://badgen.net/packagephobia/install/degit)](https://packagephobia.now.sh/result?p=degit) [![npm package version](https://badgen.net/npm/v/degit)](https://npm.im/degit) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v1.4%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) **degit** makes copies of git repositories. When you run `degit some-user/some-repo`, it will find the latest commit on https://github.com/some-user/some-repo and download the associated tar file to `~/.degit/some-user/some-repo/commithash.tar.gz` if it doesn't already exist locally. (This is much quicker than using `git clone`, because you're not downloading the entire git history.) _Requires Node 8 or above, because `async` and `await` are the cat's pyjamas_ ## Installation ```bash npm install -g degit ``` ## Usage ### Basics The simplest use of degit is to download the master branch of a repo from GitHub to the current working directory: ```bash degit user/repo # these commands are equivalent degit github:user/repo degit git@github.com:user/repo degit https://github.com/user/repo ``` Or you can download from GitLab and BitBucket: ```bash # download from GitLab degit gitlab:user/repo degit git@gitlab.com:user/repo degit https://gitlab.com/user/repo # download from BitBucket degit bitbucket:user/repo degit git@bitbucket.org:user/repo degit https://bitbucket.org/user/repo # download from Sourcehut degit git.sr.ht/user/repo degit git@git.sr.ht:user/repo degit https://git.sr.ht/user/repo ``` ### Specify a tag, branch or commit The default branch is `master`. ```bash degit user/repo#dev # branch degit user/repo#v1.2.3 # release tag degit user/repo#1234abcd # commit hash ```` ### Create a new folder for the project If the second argument is omitted, the repo will be cloned to the current directory. ```bash degit user/repo my-new-project ``` ### Specify a subdirectory To clone a specific subdirectory instead of the entire repo, just add it to the argument: ```bash degit user/repo/subdirectory ``` ### HTTPS proxying If you have an `https_proxy` environment variable, Degit will use it. ### Private repositories Private repos can be cloned by specifying `--mode=git` (the default is `tar`). In this mode, Degit will use `git` under the hood. It's much slower than fetching a tarball, which is why it's not the default. Note: this clones over SSH, not HTTPS. ### See all options ```bash degit --help ``` ## Not supported - Private repositories Pull requests are very welcome! ## Wait, isn't this just `git clone --depth 1`? A few salient differences: - If you `git clone`, you get a `.git` folder that pertains to the project template, rather than your project. You can easily forget to re-init the repository, and end up confusing yourself - Caching and offline support (if you already have a `.tar.gz` file for a specific commit, you don't need to fetch it again). - Less to type (`degit user/repo` instead of `git clone --depth 1 git@github.com:user/repo`) - Composability via [actions](#actions) - Future capabilities — [interactive mode](https://github.com/Rich-Harris/degit/issues/4), [friendly onboarding and postinstall scripts](https://github.com/Rich-Harris/degit/issues/6) ## JavaScript API You can also use degit inside a Node script: ```js const degit = require('degit'); const emitter = degit('user/repo', { cache: true, force: true, verbose: true, }); emitter.on('info', info => { console.log(info.message); }); emitter.clone('path/to/dest').then(() => { console.log('done'); }); ``` ## Actions You can manipulate repositories after they have been cloned with _actions_, specified in a `degit.json` file that lives at the top level of the working directory. Currently, there are two actions — `clone` and `remove`. Additional actions may be added in future. ### clone ```json // degit.json [ { "action": "clone", "src": "user/another-repo" } ] ``` This will clone `user/another-repo`, preserving the contents of the existing working directory. This allows you to, say, add a new README.md or starter file to a repo that you do not control. The cloned repo can contain its own `degit.json` actions. ### remove ```json // degit.json [ { "action": "remove", "files": ["LICENSE"] } ] ``` Remove a file at the specified path. ## See also - [zel](https://github.com/vutran/zel) by [Vu Tran](https://twitter.com/tranvu) - [gittar](https://github.com/lukeed/gittar) by [Luke Edwards](https://twitter.com/lukeed05) ## License [MIT](LICENSE.md). ================================================ FILE: appveyor.yml ================================================ # http://www.appveyor.com/docs/appveyor-yml version: '{build}' clone_depth: 10 init: - git config --global core.autocrlf false environment: matrix: # node.js - nodejs_version: 8 install: - ps: Install-Product node $env:nodejs_version - npm install build: off test_script: - node --version && npm --version - npm run lint - npm test matrix: fast_finish: false # cache: # - C:\Users\appveyor\AppData\Roaming\npm-cache -> package.json # npm cache # - node_modules -> package.json # local npm modules ================================================ FILE: degit ================================================ #!/usr/bin/env node require('./dist/bin.js'); ================================================ FILE: help.md ================================================ # _degit_ Usage: `degit [#ref] [] [options]` Fetches the `src` repo, and extracts it to `dest` (or the current directory). The `src` argument can be any of the following: ## GitHub repos user/repo github:user/repo https://github.com/user/repo ## GitLab repos gitlab:user/repo https://gitlab.com/user/repo ## BitBucket repos bitbucket:user/repo https://bitbucket.com/user/repo ## Sourcehut repos git.sr.ht/user/repo git@git.sr.ht:user/repo https://git.sr.ht/user/repo You can append a #ref to any of the above: ## Branches (defaults to master) user/repo#dev ## Tags user/repo#v1.2.3 ## Commit hashes user/repo#abcd1234 The `dest` directory (or the current directory, if unspecified) must be empty unless the `--force` option is used. Options: `--help`, `-h` Show this message `--cache`, `-c` Only use local cache `--force`, `-f` Allow non-empty destination directory `--verbose`, `-v` Extra logging `--mode=`, `-m=` Force the mode by which degit clones the repo Valid options are `tar` or `git` (uses SSH) See https://github.com/Rich-Harris/degit for more information ================================================ FILE: package.json ================================================ { "name": "degit", "version": "2.8.4", "engines": { "node": ">=8.0.0" }, "description": "Straightforward project scaffolding", "main": "dist/index.js", "bin": { "degit": "degit" }, "scripts": { "lint": "eslint --color --ignore-path .gitignore .", "dev": "npm run build -- --watch", "build": "rollup -c", "test": "mocha", "pretest": "npm run build", "prepublishOnly": "npm test" }, "repository": { "type": "git", "url": "git+https://github.com/Rich-Harris/degit.git" }, "keywords": [ "scaffolding", "template", "git" ], "author": "Rich Harris", "license": "MIT", "bugs": { "url": "https://github.com/Rich-Harris/degit/issues" }, "homepage": "https://github.com/Rich-Harris/degit#readme", "devDependencies": { "@rollup/plugin-commonjs": "18.0.0", "@rollup/plugin-node-resolve": "11.2.1", "chalk": "4.1.0", "enquirer": "2.3.6", "eslint": "7.23.0", "eslint-config-prettier": "8.1.0", "eslint-plugin-import": "2.22.1", "fuzzysearch": "1.0.3", "home-or-tmp": "3.0.0", "https-proxy-agent": "5.0.0", "husky": "6.0.0", "lint-staged": "10.5.4", "mocha": "8.3.2", "mri": "1.1.6", "prettier": "2.2.1", "rimraf": "3.0.2", "rollup": "2.44.0", "rollup-plugin-commonjs": "10.1.0", "sander": "0.6.0", "source-map-support": "0.5.19", "tar": "6.1.0", "tiny-glob": "0.2.8" }, "files": [ "help.md", "dist" ], "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.{js}": [ "eslint --fix", "git add" ], "*.{js, json, yml, md}": [ "prettier --write", "git add" ] } } ================================================ FILE: rollup.config.js ================================================ import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import { builtinModules } from 'module'; import pkg from './package.json'; export default { input: { index: 'src/index.js', bin: 'src/bin.js' }, output: { dir: 'dist', entryFileNames: '[name].js', chunkFileNames: '[name]-[hash].js', format: 'cjs', exports: 'auto', sourcemap: true }, external: Object.keys(pkg.dependencies || {}).concat(builtinModules), plugins: [resolve(), commonjs()] }; ================================================ FILE: src/bin.js ================================================ import fs from 'fs'; import path from 'path'; import chalk from 'chalk'; import mri from 'mri'; import glob from 'tiny-glob/sync.js'; import fuzzysearch from 'fuzzysearch'; import enquirer from 'enquirer'; import degit from './index.js'; import { tryRequire, base } from './utils.js'; const args = mri(process.argv.slice(2), { alias: { f: 'force', c: 'cache', v: 'verbose', m: 'mode' }, boolean: ['force', 'cache', 'verbose'] }); const [src, dest = '.'] = args._; async function main() { if (args.help) { const help = fs .readFileSync(path.join(__dirname, 'help.md'), 'utf-8') .replace(/^(\s*)#+ (.+)/gm, (m, s, _) => s + chalk.bold(_)) .replace(/_([^_]+)_/g, (m, _) => chalk.underline(_)) .replace(/`([^`]+)`/g, (m, _) => chalk.cyan(_)); process.stdout.write(`\n${help}\n`); } else if (!src) { // interactive mode const accessLookup = new Map(); glob(`**/access.json`, { cwd: base }).forEach(file => { const [host, user, repo] = file.split(path.sep); const json = fs.readFileSync(`${base}/${file}`, 'utf-8'); const logs = JSON.parse(json); Object.entries(logs).forEach(([ref, timestamp]) => { const id = `${host}:${user}/${repo}#${ref}`; accessLookup.set(id, new Date(timestamp).getTime()); }); }); const getChoice = file => { const [host, user, repo] = file.split(path.sep); return Object.entries(tryRequire(`${base}/${file}`)).map( ([ref, hash]) => ({ name: hash, message: `${host}:${user}/${repo}#${ref}`, value: `${host}:${user}/${repo}#${ref}` }) ); }; const choices = glob(`**/map.json`, { cwd: base }) .map(getChoice) .reduce( (accumulator, currentValue) => accumulator.concat(currentValue), [] ) .sort((a, b) => { const aTime = accessLookup.get(a.value) || 0; const bTime = accessLookup.get(b.value) || 0; return bTime - aTime; }); const options = await enquirer.prompt([ { type: 'autocomplete', name: 'src', message: 'Repo to clone?', suggest: (input, choices) => choices.filter(({ value }) => fuzzysearch(input, value)), choices }, { type: 'input', name: 'dest', message: 'Destination directory?', initial: '.' }, { type: 'toggle', name: 'cache', message: 'Use cached version?' } ]); const empty = !fs.existsSync(options.dest) || fs.readdirSync(options.dest).length === 0; if (!empty) { const { force } = await enquirer.prompt([ { type: 'toggle', name: 'force', message: 'Overwrite existing files?' } ]); if (!force) { console.error(chalk.magenta(`! Directory not empty — aborting`)); return; } } run(options.src, options.dest, { force: true, cache: options.cache }); } else { run(src, dest, args); } } function run(src, dest, args) { const d = degit(src, args); d.on('info', event => { console.error(chalk.cyan(`> ${event.message.replace('options.', '--')}`)); }); d.on('warn', event => { console.error( chalk.magenta(`! ${event.message.replace('options.', '--')}`) ); }); d.clone(dest).catch(err => { console.error(chalk.red(`! ${err.message.replace('options.', '--')}`)); process.exit(1); }); } main(); ================================================ FILE: src/index.js ================================================ import fs from 'fs'; import path from 'path'; import tar from 'tar'; import EventEmitter from 'events'; import chalk from 'chalk'; import { rimrafSync } from 'sander'; import { DegitError, exec, fetch, mkdirp, tryRequire, stashFiles, unstashFiles, degitConfigName, base } from './utils.js'; const validModes = new Set(['tar', 'git']); export default function degit(src, opts) { return new Degit(src, opts); } class Degit extends EventEmitter { constructor(src, opts = {}) { super(); this.src = src; this.cache = opts.cache; this.force = opts.force; this.verbose = opts.verbose; this.proxy = process.env.https_proxy; // TODO allow setting via --proxy this.repo = parse(src); this.mode = opts.mode || this.repo.mode; if (!validModes.has(this.mode)) { throw new Error(`Valid modes are ${Array.from(validModes).join(', ')}`); } this._hasStashed = false; this.directiveActions = { clone: async (dir, dest, action) => { if (this._hasStashed === false) { stashFiles(dir, dest); this._hasStashed = true; } const opts = Object.assign( { force: true }, { cache: action.cache, verbose: action.verbose } ); const d = degit(action.src, opts); d.on('info', event => { console.error( chalk.cyan(`> ${event.message.replace('options.', '--')}`) ); }); d.on('warn', event => { console.error( chalk.magenta(`! ${event.message.replace('options.', '--')}`) ); }); await d.clone(dest).catch(err => { console.error(chalk.red(`! ${err.message}`)); process.exit(1); }); }, remove: this.remove.bind(this) }; } _getDirectives(dest) { const directivesPath = path.resolve(dest, degitConfigName); const directives = tryRequire(directivesPath, { clearCache: true }) || false; if (directives) { fs.unlinkSync(directivesPath); } return directives; } async clone(dest) { this._checkDirIsEmpty(dest); const { repo } = this; const dir = path.join(base, repo.site, repo.user, repo.name); if (this.mode === 'tar') { await this._cloneWithTar(dir, dest); } else { await this._cloneWithGit(dir, dest); } this._info({ code: 'SUCCESS', message: `cloned ${chalk.bold(repo.user + '/' + repo.name)}#${chalk.bold( repo.ref )}${dest !== '.' ? ` to ${dest}` : ''}`, repo, dest }); const directives = this._getDirectives(dest); if (directives) { for (const d of directives) { // TODO, can this be a loop with an index to pass for better error messages? await this.directiveActions[d.action](dir, dest, d); } if (this._hasStashed === true) { unstashFiles(dir, dest); } } } remove(dir, dest, action) { let files = action.files; if (!Array.isArray(files)) { files = [files]; } const removedFiles = files .map(file => { const filePath = path.resolve(dest, file); if (fs.existsSync(filePath)) { const isDir = fs.lstatSync(filePath).isDirectory(); if (isDir) { rimrafSync(filePath); return file + '/'; } else { fs.unlinkSync(filePath); return file; } } else { this._warn({ code: 'FILE_DOES_NOT_EXIST', message: `action wants to remove ${chalk.bold( file )} but it does not exist` }); return null; } }) .filter(d => d); if (removedFiles.length > 0) { this._info({ code: 'REMOVED', message: `removed: ${chalk.bold( removedFiles.map(d => chalk.bold(d)).join(', ') )}` }); } } _checkDirIsEmpty(dir) { try { const files = fs.readdirSync(dir); if (files.length > 0) { if (this.force) { this._info({ code: 'DEST_NOT_EMPTY', message: `destination directory is not empty. Using options.force, continuing` }); } else { throw new DegitError( `destination directory is not empty, aborting. Use options.force to override`, { code: 'DEST_NOT_EMPTY' } ); } } else { this._verbose({ code: 'DEST_IS_EMPTY', message: `destination directory is empty` }); } } catch (err) { if (err.code !== 'ENOENT') throw err; } } _info(info) { this.emit('info', info); } _warn(info) { this.emit('warn', info); } _verbose(info) { if (this.verbose) this._info(info); } async _getHash(repo, cached) { try { const refs = await fetchRefs(repo); if (repo.ref === 'HEAD') { return refs.find(ref => ref.type === 'HEAD').hash; } return this._selectRef(refs, repo.ref); } catch (err) { this._warn(err); this._verbose(err.original); return this._getHashFromCache(repo, cached); } } _getHashFromCache(repo, cached) { if (repo.ref in cached) { const hash = cached[repo.ref]; this._info({ code: 'USING_CACHE', message: `using cached commit hash ${hash}` }); return hash; } } _selectRef(refs, selector) { for (const ref of refs) { if (ref.name === selector) { this._verbose({ code: 'FOUND_MATCH', message: `found matching commit hash: ${ref.hash}` }); return ref.hash; } } if (selector.length < 8) return null; for (const ref of refs) { if (ref.hash.startsWith(selector)) return ref.hash; } } async _cloneWithTar(dir, dest) { const { repo } = this; const cached = tryRequire(path.join(dir, 'map.json')) || {}; const hash = this.cache ? this._getHashFromCache(repo, cached) : await this._getHash(repo, cached); const subdir = repo.subdir ? `${repo.name}-${hash}${repo.subdir}` : null; if (!hash) { // TODO 'did you mean...?' throw new DegitError(`could not find commit hash for ${repo.ref}`, { code: 'MISSING_REF', ref: repo.ref }); } const file = `${dir}/${hash}.tar.gz`; const url = repo.site === 'gitlab' ? `${repo.url}/repository/archive.tar.gz?ref=${hash}` : repo.site === 'bitbucket' ? `${repo.url}/get/${hash}.tar.gz` : `${repo.url}/archive/${hash}.tar.gz`; try { if (!this.cache) { try { fs.statSync(file); this._verbose({ code: 'FILE_EXISTS', message: `${file} already exists locally` }); } catch (err) { mkdirp(path.dirname(file)); if (this.proxy) { this._verbose({ code: 'PROXY', message: `using proxy ${this.proxy}` }); } this._verbose({ code: 'DOWNLOADING', message: `downloading ${url} to ${file}` }); await fetch(url, file, this.proxy); } } } catch (err) { throw new DegitError(`could not download ${url}`, { code: 'COULD_NOT_DOWNLOAD', url, original: err }); } updateCache(dir, repo, hash, cached); this._verbose({ code: 'EXTRACTING', message: `extracting ${ subdir ? repo.subdir + ' from ' : '' }${file} to ${dest}` }); mkdirp(dest); await untar(file, dest, subdir); } async _cloneWithGit(dir, dest) { await exec(`git clone ${this.repo.ssh} ${dest}`); await exec(`rm -rf ${path.resolve(dest, '.git')}`); } } const supported = new Set(['github', 'gitlab', 'bitbucket', 'git.sr.ht']); function parse(src) { const match = /^(?:(?:https:\/\/)?([^:/]+\.[^:/]+)\/|git@([^:/]+)[:/]|([^/]+):)?([^/\s]+)\/([^/\s#]+)(?:((?:\/[^/\s#]+)+))?(?:\/)?(?:#(.+))?/.exec( src ); if (!match) { throw new DegitError(`could not parse ${src}`, { code: 'BAD_SRC' }); } const site = (match[1] || match[2] || match[3] || 'github').replace( /\.(com|org)$/, '' ); if (!supported.has(site)) { throw new DegitError( `degit supports GitHub, GitLab, Sourcehut and BitBucket`, { code: 'UNSUPPORTED_HOST' } ); } const user = match[4]; const name = match[5].replace(/\.git$/, ''); const subdir = match[6]; const ref = match[7] || 'HEAD'; const domain = `${site}.${ site === 'bitbucket' ? 'org' : site === 'git.sr.ht' ? '' : 'com' }`; const url = `https://${domain}/${user}/${name}`; const ssh = `git@${domain}:${user}/${name}`; const mode = supported.has(site) ? 'tar' : 'git'; return { site, user, name, ref, url, ssh, subdir, mode }; } async function untar(file, dest, subdir = null) { return tar.extract( { file, strip: subdir ? subdir.split('/').length : 1, C: dest }, subdir ? [subdir] : [] ); } async function fetchRefs(repo) { try { const { stdout } = await exec(`git ls-remote ${repo.url}`); return stdout .split('\n') .filter(Boolean) .map(row => { const [hash, ref] = row.split('\t'); if (ref === 'HEAD') { return { type: 'HEAD', hash }; } const match = /refs\/(\w+)\/(.+)/.exec(ref); if (!match) throw new DegitError(`could not parse ${ref}`, { code: 'BAD_REF' }); return { type: match[1] === 'heads' ? 'branch' : match[1] === 'refs' ? 'ref' : match[1], name: match[2], hash }; }); } catch (error) { throw new DegitError(`could not fetch remote ${repo.url}`, { code: 'COULD_NOT_FETCH', url: repo.url, original: error }); } } function updateCache(dir, repo, hash, cached) { // update access logs const logs = tryRequire(path.join(dir, 'access.json')) || {}; logs[repo.ref] = new Date().toISOString(); fs.writeFileSync( path.join(dir, 'access.json'), JSON.stringify(logs, null, ' ') ); if (cached[repo.ref] === hash) return; const oldHash = cached[repo.ref]; if (oldHash) { let used = false; for (const key in cached) { if (cached[key] === hash) { used = true; break; } } if (!used) { // we no longer need this tar file try { fs.unlinkSync(path.join(dir, `${oldHash}.tar.gz`)); } catch (err) { // ignore } } } cached[repo.ref] = hash; fs.writeFileSync( path.join(dir, 'map.json'), JSON.stringify(cached, null, ' ') ); } ================================================ FILE: src/utils.js ================================================ import fs from 'fs'; import path from 'path'; import homeOrTmp from 'home-or-tmp'; import https from 'https'; import child_process from 'child_process'; import URL from 'url'; import Agent from 'https-proxy-agent'; import { rimrafSync, copydirSync } from 'sander'; const tmpDirName = 'tmp'; const degitConfigName = 'degit.json'; export { degitConfigName }; export class DegitError extends Error { constructor(message, opts) { super(message); Object.assign(this, opts); } } export function tryRequire(file, opts) { try { if (opts && opts.clearCache === true) { delete require.cache[require.resolve(file)]; } return require(file); } catch (err) { return null; } } export function exec(command) { return new Promise((fulfil, reject) => { child_process.exec(command, (err, stdout, stderr) => { if (err) { reject(err); return; } fulfil({ stdout, stderr }); }); }); } export function mkdirp(dir) { const parent = path.dirname(dir); if (parent === dir) return; mkdirp(parent); try { fs.mkdirSync(dir); } catch (err) { if (err.code !== 'EEXIST') throw err; } } export function fetch(url, dest, proxy) { return new Promise((fulfil, reject) => { let options = url; if (proxy) { const parsedUrl = URL.parse(url); options = { hostname: parsedUrl.host, path: parsedUrl.path, agent: new Agent(proxy) }; } https .get(options, response => { const code = response.statusCode; if (code >= 400) { reject({ code, message: response.statusMessage }); } else if (code >= 300) { fetch(response.headers.location, dest, proxy).then(fulfil, reject); } else { response .pipe(fs.createWriteStream(dest)) .on('finish', () => fulfil()) .on('error', reject); } }) .on('error', reject); }); } export function stashFiles(dir, dest) { const tmpDir = path.join(dir, tmpDirName); rimrafSync(tmpDir); mkdirp(tmpDir); fs.readdirSync(dest).forEach(file => { const filePath = path.join(dest, file); const targetPath = path.join(tmpDir, file); const isDir = fs.lstatSync(filePath).isDirectory(); if (isDir) { copydirSync(filePath).to(targetPath); rimrafSync(filePath); } else { fs.copyFileSync(filePath, targetPath); fs.unlinkSync(filePath); } }); } export function unstashFiles(dir, dest) { const tmpDir = path.join(dir, tmpDirName); fs.readdirSync(tmpDir).forEach(filename => { const tmpFile = path.join(tmpDir, filename); const targetPath = path.join(dest, filename); const isDir = fs.lstatSync(tmpFile).isDirectory(); if (isDir) { copydirSync(tmpFile).to(targetPath); rimrafSync(tmpFile); } else { if (filename !== 'degit.json') { fs.copyFileSync(tmpFile, targetPath); } fs.unlinkSync(tmpFile); } }); rimrafSync(tmpDir); } export const base = path.join(homeOrTmp, '.degit'); ================================================ FILE: test/test.js ================================================ require('source-map-support').install(); const fs = require('fs'); const path = require('path'); const glob = require('tiny-glob/sync'); const rimraf = require('rimraf').sync; const assert = require('assert'); const child_process = require('child_process'); const degit = require('../dist/index.js'); const degitPath = path.resolve('dist/bin.js'); const timeout = 30000; function exec(cmd) { return new Promise((fulfil, reject) => { child_process.exec(cmd, (err, stdout, stderr) => { if (err) return reject(err); console.log(stdout); console.error(stderr); fulfil(); }); }); } describe('degit', function() { this.timeout(timeout); function compare(dir, files) { const expected = glob('**', { cwd: dir }); assert.deepEqual(Object.keys(files).sort(), expected.sort()); expected.forEach(file => { if (!fs.lstatSync(`${dir}/${file}`).isDirectory()) { assert.equal(files[file].trim(), read(`${dir}/${file}`).trim()); } }); } beforeEach(async () => await rimraf('.tmp')); afterEach(async () => await rimraf('.tmp')); describe('github', () => { [ 'mhkeller/degit-test-repo-compose', 'Rich-Harris/degit-test-repo', 'github:Rich-Harris/degit-test-repo', 'git@github.com:Rich-Harris/degit-test-repo', 'https://github.com/Rich-Harris/degit-test-repo.git' ].forEach(src => { it(src, async () => { await exec(`node ${degitPath} ${src} .tmp/test-repo -v`); compare(`.tmp/test-repo`, { 'file.txt': 'hello from github!', subdir: null, 'subdir/file.txt': 'hello from a subdirectory!' }); }); }); }); describe('gitlab', () => { [ 'gitlab:Rich-Harris/degit-test-repo', 'git@gitlab.com:Rich-Harris/degit-test-repo', 'https://gitlab.com/Rich-Harris/degit-test-repo.git' ].forEach(src => { it(src, async () => { await exec(`node ${degitPath} ${src} .tmp/test-repo -v`); compare(`.tmp/test-repo`, { 'file.txt': 'hello from gitlab!' }); }); }); }); describe('bitbucket', () => { [ 'bitbucket:Rich_Harris/degit-test-repo', 'git@bitbucket.org:Rich_Harris/degit-test-repo', 'https://bitbucket.org/Rich_Harris/degit-test-repo.git' ].forEach(src => { it(src, async () => { await exec(`node ${degitPath} ${src} .tmp/test-repo -v`); compare(`.tmp/test-repo`, { 'file.txt': 'hello from bitbucket' }); }); }); }); describe('Sourcehut', () => { [ 'git.sr.ht/~satotake/degit-test-repo', 'https://git.sr.ht/~satotake/degit-test-repo', 'git@git.sr.ht:~satotake/degit-test-repo' ].forEach(src => { it(src, async () => { await exec(`node ${degitPath} ${src} .tmp/test-repo -v`); compare(`.tmp/test-repo`, { 'file.txt': 'hello from sourcehut!' }); }); }); }); describe('Subdirectories', () => { [ 'Rich-Harris/degit-test-repo/subdir', 'github:Rich-Harris/degit-test-repo/subdir', 'git@github.com:Rich-Harris/degit-test-repo/subdir', 'https://github.com/Rich-Harris/degit-test-repo.git/subdir' ].forEach(src => { it(src, async () => { await exec(`node ${degitPath} ${src} .tmp/test-repo -v`); compare(`.tmp/test-repo`, { 'file.txt': 'hello from a subdirectory!' }); }); }); }); describe('non-empty directories', () => { it('fails without --force', async () => { let succeeded; try { await exec(`mkdir -p .tmp/test-repo`); await exec(`echo "not empty" > .tmp/test-repo/file.txt`); await exec( `node ${degitPath} Rich-Harris/degit-test-repo .tmp/test-repo -v` ); succeeded = true; } catch (err) { assert.ok(/destination directory is not empty/.test(err.message)); } assert.ok(!succeeded); }); it('succeeds with --force', async () => { await exec( `node ${degitPath} Rich-Harris/degit-test-repo .tmp/test-repo -fv` ); }); }); describe('command line arguments', () => { it('allows flags wherever', async () => { await exec( `node ${degitPath} -v Rich-Harris/degit-test-repo .tmp/test-repo` ); compare(`.tmp/test-repo`, { 'file.txt': 'hello from github!', subdir: null, 'subdir/file.txt': 'hello from a subdirectory!' }); }); }); describe('api', () => { it('is usable from node scripts', async () => { await degit('Rich-Harris/degit-test-repo', { force: true }).clone( '.tmp/test-repo' ); compare(`.tmp/test-repo`, { 'file.txt': 'hello from github!', subdir: null, 'subdir/file.txt': 'hello from a subdirectory!' }); }); }); describe('actions', () => { it('removes specified file', async () => { await exec( `node ${degitPath} -v mhkeller/degit-test-repo-remove-only .tmp/test-repo` ); compare(`.tmp/test-repo`, {}); }); it('clones repo and removes specified file', async () => { await exec( `node ${degitPath} -v mhkeller/degit-test-repo-remove .tmp/test-repo` ); compare(`.tmp/test-repo`, { 'other.txt': 'hello from github!', subdir: null, 'subdir/file.txt': 'hello from a subdirectory!' }); }); it('removes and adds nested files', async () => { await rimraf('.tmp'); await exec( `node ${degitPath} -v mhkeller/degit-test-repo-nested-actions .tmp/test-repo` ); compare(`.tmp/test-repo`, { dir: null, folder: null, subdir: null, 'folder/file.txt': 'hello from clobber file!', 'folder/other.txt': 'hello from other file!', 'subdir/file.txt': 'hello from a subdirectory!' }); }); }); describe('git mode', () => { it('is able to clone correctly using git mode', async () => { await rimraf('.tmp'); await exec( `node ${degitPath} --mode=git https://github.com/Rich-Harris/degit-test-repo-private.git .tmp/test-repo` ); compare('.tmp/test-repo', { 'file.txt': 'hello from a private repo!' }); }); }); }); function read(file) { return fs.readFileSync(file, 'utf-8'); }