Skip to content

Commit

Permalink
feat(resolution): Allow "git+file:" dependency url protocol.
Browse files Browse the repository at this point in the history
Allow "git_file:" dependency protocol to use a local un-pushed git repo as a dependency. Fix bug
where local directories named "*.git" were treated as git dependencies

fixes yarnpkg#3670 yarnpkg#5017
  • Loading branch information
rally25rs committed Dec 10, 2017
1 parent 4a2e10b commit ba604d5
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 45 deletions.
9 changes: 9 additions & 0 deletions __tests__/commands/install/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -1177,3 +1177,12 @@ test.concurrent('install will not overwrite linked dependencies', async (): Prom
});
});
});

// There was an issue where anything ending with `.git` would be sent to GitResolver, even if it was a file: dep.
// This caused an error if you had a directory named "myModule.git" and tried to use it with "file:../myModule.git"
// See https://github.com/yarnpkg/yarn/issues/3670
test.concurrent('file: dependency ending with `.git` should work', (): Promise<void> => {
return runInstall({}, 'local-named-git', async (config, reporter) => {
expect(await fs.exists(path.join(config.cwd, 'node_modules', 'a'))).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "a",
"version": "1.0.0"
}
5 changes: 5 additions & 0 deletions __tests__/fixtures/install/local-named-git/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"a": "file:./deps/a.git"
}
}
67 changes: 67 additions & 0 deletions __tests__/resolvers/exotics/git-resolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/* @flow */

import GitResolver from '../../../src/resolvers/exotics/git-resolver.js';

test('isVersion true for ssh: protocol', () => {
expect(GitResolver.isVersion('ssh://[email protected]/sindresorhus/beeper.git')).toBe(true);
});

test('isVersion true for git: protocol', () => {
expect(GitResolver.isVersion('git://[email protected]/sindresorhus/beeper.git')).toBe(true);
});

test('isVersion true for git+ssh: protocol', () => {
expect(GitResolver.isVersion('git+ssh://[email protected]/sindresorhus/beeper.git')).toBe(true);
});

test('isVersion true for http: protocol when ends with .git', () => {
expect(GitResolver.isVersion('http://example.com/sindresorhus/beeper.git')).toBe(true);
});

test('isVersion true for http: protocol when ends with .git and hash', () => {
expect(GitResolver.isVersion('http://example.com/sindresorhus/beeper.git#branch')).toBe(true);
});

test('isVersion false for http: protocol when does not end with .git', () => {
expect(GitResolver.isVersion('http://example.com/sindresorhus/beeper')).toBe(false);
});

test('isVersion false for http: protocol when does not end with .git with hash', () => {
expect(GitResolver.isVersion('http://example.com/sindresorhus/beeper#branch')).toBe(false);
});

test('isVersion true for https: protocol when ends with .git', () => {
expect(GitResolver.isVersion('https://example.com/sindresorhus/beeper.git')).toBe(true);
});

test('isVersion true for https: protocol when ends with .git and hash', () => {
expect(GitResolver.isVersion('https://example.com/sindresorhus/beeper.git#branch')).toBe(true);
});

test('isVersion false for https: protocol when does not end with .git', () => {
expect(GitResolver.isVersion('https://example.com/sindresorhus/beeper')).toBe(false);
});

test('isVersion false for https: protocol when does not end with .git with hash', () => {
expect(GitResolver.isVersion('https://example.com/sindresorhus/beeper#branch')).toBe(false);
});

test('isVersion false for file: protocol when ends with .git', () => {
expect(GitResolver.isVersion('file:../project.git')).toBe(false);
});

test('isVersion true for any github.com repo url', () => {
expect(GitResolver.isVersion('foo://github.com/sindresorhus/beeper')).toBe(true);
});

test('isVersion true for any gitlab.com repo url', () => {
expect(GitResolver.isVersion('foo://gitlab.com/sindresorhus/beeper')).toBe(true);
});

test('isVersion true for any bitbucket.com repo url', () => {
expect(GitResolver.isVersion('foo://bitbucket.com/sindresorhus/beeper')).toBe(true);
});

test('isVersion false for any github url to archive file', () => {
expect(GitResolver.isVersion('http://github.com/sindresorhus/beeper/archive/v1.0.0.tar.gz')).toBe(false);
});
7 changes: 6 additions & 1 deletion __tests__/util/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ test('npmUrlToGitUrl', () => {
hostname: 'github.com',
repository: 'ssh://[email protected]/npm-opam/ocamlfind',
});
expect(Git.npmUrlToGitUrl('file:../ocalmfind.git')).toEqual({
expect(Git.npmUrlToGitUrl('git+file:../ocalmfind.git')).toEqual({
protocol: 'file:',
hostname: null,
repository: '../ocalmfind.git',
});
expect(Git.npmUrlToGitUrl('git+file:../ocalmfind')).toEqual({
protocol: 'file:',
hostname: null,
repository: '../ocalmfind',
Expand Down
43 changes: 11 additions & 32 deletions src/resolvers/exotics/git-resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,10 @@ import Git from '../../util/git.js';

const urlParse = require('url').parse;

const GIT_PROTOCOL_PATTERN = /git\+.+:/;

// we purposefully omit https and http as those are only valid if they end in the .git extension
const GIT_PROTOCOLS = ['git:', 'ssh:'];

const GIT_HOSTS = ['github.com', 'gitlab.com', 'bitbucket.com', 'bitbucket.org'];

const GIT_PATTERN_MATCHERS = [/^git:/, /^git\+.+:/, /^ssh:/, /^https?:.+\.git$/, /^https?:.+\.git#.+/];

export default class GitResolver extends ExoticResolver {
constructor(request: PackageRequest, fragment: string) {
super(request, fragment);
Expand All @@ -32,35 +29,17 @@ export default class GitResolver extends ExoticResolver {
hash: string;

static isVersion(pattern: string): boolean {
const parts = urlParse(pattern);

// this pattern hasn't been exploded yet, we'll hit this code path again later once
// we've been normalized #59
if (!parts.protocol) {
return false;
}

const pathname = parts.pathname;
if (pathname && pathname.endsWith('.git')) {
// ends in .git
return true;
}

if (GIT_PROTOCOL_PATTERN.test(parts.protocol)) {
return true;
}

if (GIT_PROTOCOLS.indexOf(parts.protocol) >= 0) {
return true;
for (const matcher of GIT_PATTERN_MATCHERS) {
if (matcher.test(pattern)) {
return true;
}
}

if (parts.hostname && parts.path) {
const path = parts.path;
if (GIT_HOSTS.indexOf(parts.hostname) >= 0) {
// only if dependency is pointing to a git repo,
// e.g. facebook/flow and not file in a git repo facebook/flow/archive/v1.0.0.tar.gz
return path.split('/').filter((p): boolean => !!p).length === 2;
}
const {hostname, path} = urlParse(pattern);
if (hostname && path && GIT_HOSTS.indexOf(hostname) >= 0) {
// only if dependency is pointing to a git repo,
// e.g. facebook/flow and not file in a git repo facebook/flow/archive/v1.0.0.tar.gz
return path.split('/').filter((p): boolean => !!p).length === 2;
}

return false;
Expand Down
19 changes: 7 additions & 12 deletions src/util/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {resolveVersion, isCommitSha, parseRefs} from './git/git-ref-resolver.js'
import * as crypto from './crypto.js';
import * as fs from './fs.js';
import map from './map.js';
import {removePrefix} from './misc.js';

const GIT_PROTOCOL_PREFIX = 'git+';
const SSH_PROTOCOL = 'ssh:';
Expand Down Expand Up @@ -85,18 +86,15 @@ export default class Git implements GitRefResolvingInterface {
* git "URLs" also allow an alternative scp-like syntax, so they're not standard URLs.
*/
static npmUrlToGitUrl(npmUrl: string): GitUrl {
// Expand shortened format first if needed
npmUrl = removePrefix(npmUrl, GIT_PROTOCOL_PREFIX);

let parsed = url.parse(npmUrl);
const expander = parsed.protocol && SHORTHAND_SERVICES[parsed.protocol];

if (expander) {
parsed = expander(parsed);
}

if (parsed.protocol && parsed.protocol.startsWith(GIT_PROTOCOL_PREFIX)) {
parsed.protocol = parsed.protocol.slice(GIT_PROTOCOL_PREFIX.length);
}

// Special case in npm, where ssh:// prefix is stripped to pass scp-like syntax
// which in git works as remote path only if there are no slashes before ':'.
// See #3146.
Expand All @@ -116,21 +114,18 @@ export default class Git implements GitRefResolvingInterface {
};
}

// npm local packages are specified as FILE_PROTOCOL, but url parser interprets them as using the file protocol.
// This changes the behavior so that git doesn't see this as a hostname, but as a file path.
// See #3670.
// git local repos are specified as `git+file:` and a filesystem path, not a url.
let repository;
if (parsed.protocol === FILE_PROTOCOL && !parsed.hostname && parsed.path && parsed.port === null) {
// for local repos, remove trailing `.git` because it is used as `cwd` path for `git show-ref`
repository = parsed.path.replace(/\.git$/, '');
if (parsed.protocol === FILE_PROTOCOL) {
repository = parsed.path;
} else {
repository = url.format({...parsed, hash: ''});
}

return {
hostname: parsed.hostname || null,
protocol: parsed.protocol || FILE_PROTOCOL,
repository,
repository: repository || '',
};
}

Expand Down

0 comments on commit ba604d5

Please sign in to comment.