diff --git a/index.js b/index.js index 43cdfbf..4ac85e6 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,7 @@ var fs = require('fs'); var path = require('path'); +var expand = require('expand-tilde'); var exists = require('fs-exists-sync'); var extend = require('extend-shallow'); var configPath = require('git-config-path'); @@ -33,23 +34,36 @@ var ini = require('ini'); function parse(options, cb) { if (typeof options === 'function') { cb = options; - options = {}; + options = null; } if (typeof cb !== 'function') { - throw new TypeError('parse-git-config async expects a callback function.'); + throw new TypeError('expected callback to be a function'); } - options = options || {}; - var filepath = parse.resolve(options); + var filepath = parse.resolveConfigPath(options); + if (filepath === null) { + cb(); + return; + } fs.stat(filepath, function(err, stat) { - if (err) return cb(err); + if (err) { + cb(err); + return; + } fs.readFile(filepath, 'utf8', function(err, str) { - if (err) return cb(err); - var parsed = ini.parse(str); - cb(null, parsed); + if (err) { + cb(err); + return; + } + + if (options && options.include === true) { + str = injectInclude(str, path.resolve(path.dirname(filepath))); + } + + cb(null, ini.parse(str)); }); }); } @@ -69,12 +83,17 @@ function parse(options, cb) { */ parse.sync = function parseSync(options) { - options = options || {}; - var filepath = parse.resolve(options); - + var filepath = parse.resolveConfigPath(options); if (filepath && exists(filepath)) { - var str = fs.readFileSync(filepath, 'utf8'); - return ini.parse(str); + var input = fs.readFileSync(filepath, 'utf8'); + + if (options && options.include === true) { + var cwd = path.resolve(path.dirname(filepath)); + var str = injectInclude(input, cwd); + return ini.parse(str); + } + + return ini.parse(input); } return {}; }; @@ -83,15 +102,23 @@ parse.sync = function parseSync(options) { * Resolve the git config path */ -parse.resolve = function resolve(options) { +parse.resolveConfigPath = function(options) { if (typeof options === 'string') { options = { type: options }; } var opts = extend({cwd: process.cwd()}, options); - var fp = opts.path || configPath(opts.type); + var fp = opts.path ? expand(opts.path) : configPath(opts.type); return fp ? path.resolve(opts.cwd, fp) : null; }; +/** + * Deprecated: use `.resolveConfigPath` instead + */ + +parse.resolve = function(options) { + return parse.resolveConfigPath(options); +}; + /** * Returns an object with only the properties that had ini-style keys * converted to objects (example below). @@ -118,6 +145,38 @@ parse.keys = function parseKeys(config) { return res; }; +function injectInclude(input, cwd) { + var pathRegex = /^\s*path\s*=\s*/; + var lines = input.split('\n'); + var len = lines.length; + var filepath = ''; + var res = []; + + for (var i = 0; i < len; i++) { + var line = lines[i]; + var n = i; + + if (line.indexOf('[include]') === 0) { + while (n < len && !pathRegex.test(filepath)) { + filepath = lines[++n]; + } + + if (!filepath) { + return input; + } + + filepath = filepath.replace(pathRegex, ''); + var fp = path.resolve(cwd, expand(filepath)); + res.push(fs.readFileSync(fp)); + + } else { + res.push(line); + } + } + + return res.join('\n'); +} + /** * Expose `parse` */ diff --git a/package.json b/package.json index b90f9f1..d595282 100644 --- a/package.json +++ b/package.json @@ -25,15 +25,16 @@ "test": "mocha" }, "dependencies": { - "extend-shallow": "^2.0.1", + "expand-tilde": "^2.0.2", + "extend-shallow": "^3.0.2", "fs-exists-sync": "^0.1.0", "git-config-path": "^1.0.1", - "ini": "^1.3.4" + "ini": "^1.3.5" }, "devDependencies": { - "gulp-format-md": "^0.1.11", - "homedir-polyfill": "^1.0.1", - "mocha": "^3.2.0" + "gulp-format-md": "^1.0.0", + "mocha": "^3.5.3", + "homedir-polyfill": "^1.0.1" }, "keywords": [ "config", diff --git a/test/expected/_gitconfig.js b/test/expected/_gitconfig.js new file mode 100644 index 0000000..70dd302 --- /dev/null +++ b/test/expected/_gitconfig.js @@ -0,0 +1,122 @@ +module.exports = { + user: { + email: 'email', + name: 'name', + signingkey: 'https://help.github.com/articles/generating-a-new-gpg-key/' + }, + github: { + user: 'name', + token: 'https://github.com/settings/tokens' + }, + commit: { + gpgsign: true + }, + tag: { + gpgsign: true, + path: '_gitconfig.local', + sort: 'version:refname' + }, + core: { + legacyheaders: false, + quotepath: false, + trustctime: false, + precomposeunicode: false, + pager: 'cat', + logAllRefUpdates: true, + excludesfile: '~/.gitignore' + }, + repack: { + usedeltabaseoffset: true + }, + merge: { + log: true, + conflictstyle: 'diff3' + }, + apply: { + whitespace: 'fix' + }, + help: { + autocorrect: '1' + }, + rerere: { + enabled: true + }, + color: { + diff: 'auto', + status: 'auto', + branch: 'auto', + interactive: 'auto', + ui: 'always' + }, + 'color "diff"': { + meta: 'yellow bold', + frag: 'magenta', + plain: 'white bold', + old: 'red bold', + new: 'green bold', + commit: 'yellow bold', + func: 'green dim', + whitespace: 'red reverse' + }, + 'color "status"': { + added: 'yellow', + changed: 'green', + untracked: 'cyan' + }, + 'color "branch"': { + current: 'yellow reverse', + local: 'yellow', + remote: 'green' + }, + diff: { + renames: 'copies', + algorithm: 'patience', + compactionHeuristic: true, + wsErrorHighlight: 'all' + }, + 'diff "bin"': { + textconv: 'hexdump -v -C' + }, + credential: { + helper: 'store' + }, + status: { + relativePaths: true, + showUntrackedFiles: 'no' + }, + pull: { + rebase: true + }, + push: { + default: 'current', + followTags: true + }, + alias: { + a: 'commit --amend', + c: 'commit -am', + d: '!git diff --exit-code && git diff --cached', + dif: 'diff', + git: '!exec git', + p: 'push -u', + r: 'reset --soft HEAD~1', + s: 'status', + sc: 'clone --depth=1', + l: 'log --graph --pretty=format:\'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset\' --abbrev-commit -n 15' + }, + 'remote "origin"': { + fetch: '+refs/tags/*:refs/tags/*' + }, + branch: { + autosetupmerge: 'always', + autosetuprebase: 'always' + }, + http: { + sslverify: false + }, + submodule: { + fetchJobs: '0' + }, + fetch: { + prune: true + } +}; diff --git a/test/fixtures/_gitconfig b/test/fixtures/_gitconfig new file mode 100644 index 0000000..7b999ac --- /dev/null +++ b/test/fixtures/_gitconfig @@ -0,0 +1,86 @@ +[include] + path = _gitconfig.local +[core] + legacyheaders = false + quotepath = false + trustctime = false + precomposeunicode = false + pager = cat + logAllRefUpdates = true + excludesfile = ~/.gitignore +[repack] + usedeltabaseoffset = true +[merge] + log = true + conflictstyle = diff3 +[apply] + whitespace = fix +[help] + autocorrect = 1 +[rerere] + enabled = true +[color] + diff = auto + status = auto + branch = auto + interactive = auto + ui = always +[color "diff"] + meta = yellow bold + frag = magenta + plain = white bold + old = red bold + new = green bold + commit = yellow bold + func = green dim + whitespace = red reverse +[color "status"] + added = yellow + changed = green + untracked = cyan +[color "branch"] + current = yellow reverse + local = yellow + remote = green +[diff] + renames = copies + algorithm = patience + compactionHeuristic = true + wsErrorHighlight = all +[diff "bin"] + textconv = hexdump -v -C +[credential] + helper = store +[status] + relativePaths = true + showUntrackedFiles = no +[pull] + rebase = true +[push] + default = current + followTags = true +[alias] + a = commit --amend + c = commit -am + d = !git diff --exit-code && git diff --cached + dif = diff + git = !exec git + p = push -u + r = reset --soft HEAD~1 + s = status + sc = clone --depth=1 + l = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit -n 15 +[remote "origin"] + fetch = +refs/pr/*/head:refs/remotes/origin/pr/* + fetch = +refs/tags/*:refs/tags/* +[branch] + autosetupmerge = always + autosetuprebase = always +[http] + sslverify = false +[submodule] + fetchJobs = 0 +[fetch] + prune = true +[tag] + sort = version:refname diff --git a/test/fixtures/_gitconfig.local b/test/fixtures/_gitconfig.local new file mode 100644 index 0000000..afa9a19 --- /dev/null +++ b/test/fixtures/_gitconfig.local @@ -0,0 +1,11 @@ +[user] + email = email + name = name + signingkey = https://help.github.com/articles/generating-a-new-gpg-key/ +[github] + user = name + token = https://github.com/settings/tokens +[commit] + gpgsign = true +[tag] + gpgsign = true \ No newline at end of file diff --git a/test.js b/test/test.js similarity index 64% rename from test.js rename to test/test.js index 1f63fc3..c00c66c 100644 --- a/test.js +++ b/test/test.js @@ -1,7 +1,7 @@ /*! * parse-git-config * - * Copyright (c) 2015 Jon Schlinkert. + * Copyright (c) 2015-2018 Jon Schlinkert. * Licensed under the MIT license. */ @@ -9,11 +9,16 @@ require('mocha'); var isTravis = process.env.TRAVIS || process.env.CLI; +var fs = require('fs'); var os = require('os'); var assert = require('assert'); var path = require('path'); var homedir = require('homedir-polyfill'); -var parse = require('./'); +var parse = require('..'); + +function read(filepath) { + return fs.readFileSync(path.join(__dirname, filepath), 'utf8'); +} describe('sync:', function() { it('should return an object', function() { @@ -22,14 +27,10 @@ describe('sync:', function() { }); describe('async:', function() { - it('should throw a callback is not passed:', function(cb) { - try { + it('should throw a callback is not passed:', function() { + assert.throws(function() { parse(); - cb(new Error('expected an error')); - } catch (err) { - assert.equal(err.message, 'parse-git-config async expects a callback function.'); - cb(); - } + }, /expected/); }); it('should parse .git/config', function(cb) { @@ -40,8 +41,18 @@ describe('async:', function() { }); }); + it('should include other config sources', function() { + var fp = path.join(__dirname, 'fixtures/_gitconfig'); + + parse({ path: fp, include: true }, function(err, config) { + assert(!err); + assert.deepEqual(config, require('./expected/_gitconfig.js')); + cb(); + }); + }); + it('should throw an error when .git/config does not exist:', function(cb) { - parse({path: 'foo'}, function(err, config) { + parse({ path: 'foo' }, function(err, config) { assert(err instanceof Error); assert(/ENOENT.*parse-git-config/.test(err.message)); cb(); @@ -56,11 +67,17 @@ describe('resolve:', function() { it('should allow override path', function() { var fp = path.resolve(homedir(), '.gitconfig'); - assert.equal(parse.resolve({path: fp}), fp); + assert.equal(parse.resolve({ path: fp }), fp); + }); + + it('should include other config sources', function() { + var fp = path.join(__dirname, 'fixtures/_gitconfig'); + var actual = parse.sync({ path: fp, include: true }); + assert.deepEqual(actual, require('./expected/_gitconfig.js')); }); it('should resolve relative path to cwd', function() { - assert.equal(parse.resolve({path: '.config'}), path.resolve(process.cwd(), '.config')); + assert.equal(parse.resolve({ path: '.config' }), path.resolve(process.cwd(), '.config')); }); it('should resolve relative path to the global git config when `global` is passed', function() { @@ -69,7 +86,7 @@ describe('resolve:', function() { }); it('should allow override of cwd', function() { - var actual = parse.resolve({path: '.config', cwd: '/opt/config'}); + var actual = parse.resolve({ path: '.config', cwd: '/opt/config' }); assert.equal(actual, path.resolve('/opt/config/.config')); }); });