├── .gitignore ├── src ├── config.coffee ├── git-cli.coffee ├── errors.coffee ├── runner.coffee ├── repo-class-methods.coffee ├── remote-actions.coffee ├── repository.coffee ├── branch.coffee ├── util.coffee ├── index-actions.coffee ├── remote.coffee ├── cli-command.coffee ├── cli-option.coffee ├── stats.coffee └── git-util.coffee ├── .npmignore ├── test ├── test-helpers.coffee ├── cli-option-test.coffee ├── cli-command-test.coffee ├── util-test.coffee ├── git-util-test.coffee └── repository-test.coffee ├── .travis.yml ├── LICENSE ├── Gruntfile.coffee ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /src/config.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | Promise: Promise 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .travis.yml 3 | 4 | test/ 5 | src/ 6 | 7 | -------------------------------------------------------------------------------- /src/git-cli.coffee: -------------------------------------------------------------------------------- 1 | errors = require './errors' 2 | config = require './config' 3 | 4 | module.exports = 5 | Repository: require './repository' 6 | GitError: errors.GitError 7 | BadRepositoryError: errors.BadRepositoryError 8 | config: config 9 | -------------------------------------------------------------------------------- /test/test-helpers.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs-extra' 2 | 3 | Helpers = 4 | removeFirstLine: (file) -> 5 | content = fs.readFileSync(file).toString() 6 | lines = content.split '\n' 7 | newContent = lines[1..].join '\n' 8 | fs.writeFileSync file, newContent 9 | 10 | module.exports = Helpers 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | - '6' 5 | - '5' 6 | - '4' 7 | before_script: 8 | - git config --global user.name "Daniel Perez" 9 | - git config --global user.email "tuvistavie@gmail.com" 10 | before_install: npm install -g grunt-cli 11 | script: grunt coverage 12 | notifications: 13 | email: false 14 | -------------------------------------------------------------------------------- /src/errors.coffee: -------------------------------------------------------------------------------- 1 | class GitError extends Error 2 | constructor: (@message="") -> 3 | @name = "GitError" 4 | super @message 5 | 6 | class BadRepositoryError extends GitError 7 | constructor: (@message="") -> 8 | @name = "BadRepositoryError" 9 | super @message 10 | 11 | module.exports = 12 | GitError: GitError 13 | BadRepositoryError: BadRepositoryError 14 | -------------------------------------------------------------------------------- /src/runner.coffee: -------------------------------------------------------------------------------- 1 | exec = require('child_process').exec 2 | Promise = require('./config').Promise 3 | 4 | Runner = 5 | execute: (command, options, callback) -> 6 | new Promise (resolve, reject) -> 7 | exec command.toString(), options, (err, stdout, stderr) -> 8 | result = if options.processResult then options.processResult(err, stdout, stderr) else stdout 9 | if callback? 10 | callback(err, result, stderr) 11 | if err? 12 | reject(err, result, stderr) 13 | else 14 | resolve(result, stderr) 15 | 16 | module.exports = Runner 17 | -------------------------------------------------------------------------------- /src/repo-class-methods.coffee: -------------------------------------------------------------------------------- 1 | util = require './util' 2 | CliCommand = require './cli-command' 3 | execute = require('./runner').execute 4 | fs = require 'fs-extra' 5 | 6 | exports.init = (path, options, callback) -> 7 | [options, callback] = util.setOptions options, callback 8 | fs.ensureDirSync path 9 | command = new CliCommand(['git', 'init'], path, options) 10 | execOptions = processResult: (=> new this("#{path}/.git")) 11 | execute command, execOptions, callback 12 | 13 | exports.clone = (url, path, options, callback) -> 14 | [options, callback] = util.setOptions options, callback 15 | command = new CliCommand(['git', 'clone'], [url, path], options) 16 | execOptions = processResult: (=> new this("#{path}/.git")) 17 | execute command, execOptions, callback 18 | -------------------------------------------------------------------------------- /src/remote-actions.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | util = require './util' 3 | CliCommand = require './cli-command' 4 | execute = require('./runner').execute 5 | 6 | getOptions = (args, options, callback) -> 7 | args = [args] if _.isString(args) 8 | if _.isArray(args) 9 | [options, callback] = util.setOptions(options, callback) 10 | else 11 | [[options, callback], args] = [util.setOptions(args, options), []] 12 | [args, options, callback] 13 | 14 | makeAction = (action) -> 15 | (args, options, callback) -> 16 | [args, options, callback] = getOptions(args, options, callback) 17 | command = new CliCommand(['git', action], args, options) 18 | execute command, @_getOptions(), callback 19 | 20 | for action in ['push', 'pull', 'fetch'] 21 | exports[action] = makeAction(action) 22 | -------------------------------------------------------------------------------- /src/repository.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs-extra' 2 | _ = require 'underscore' 3 | path = require 'path' 4 | 5 | errors = require './errors' 6 | CliCommand = require './cli-command' 7 | execute = require('./runner').execute 8 | util = require './util' 9 | gitUtil = require './git-util' 10 | 11 | class Repository 12 | BAD_PATH_MSG = "repository path should point .git directory" 13 | 14 | constructor: (@path) -> 15 | unless @path.endsWith('.git') 16 | throw new errors.BadRepositoryError(BAD_PATH_MSG) 17 | 18 | workingDir: -> path.dirname @path 19 | 20 | _getOptions: (extra={}) -> 21 | _.assign({cwd: @workingDir()}, extra) 22 | 23 | _.each require('./repo-class-methods'), (method, key) -> 24 | Repository[key] = method 25 | 26 | _.each ['stats', 'remote', 'branch', 'remote-actions', 'index-actions'], (module) -> 27 | _.each require("./#{module}"), (method, key) -> 28 | Repository.prototype[key] = method 29 | 30 | module.exports = Repository 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Daniel Perez 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /src/branch.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | CliCommand = require './cli-command' 3 | gitUtil = require './git-util' 4 | util = require './util' 5 | execute = require('./runner').execute 6 | 7 | exports.currentBranch = (options, callback) -> 8 | [options, callback] = util.setOptions options, callback 9 | command = new CliCommand(['git', 'branch'], options) 10 | execOptions = processResult: (err, stdout) -> gitUtil.parseCurrentBranch stdout 11 | execute command, @_getOptions(execOptions), callback 12 | 13 | exports.branch = (branch, options, callback) -> 14 | branch = [branch] if _.isString(branch) 15 | if _.isArray(branch) 16 | [[options, callback], hasName] = [util.setOptions(options, callback), true] 17 | else 18 | [[options, callback], hasName] = [util.setOptions(branch, options), false] 19 | branch = [] unless hasName 20 | command = new CliCommand(['git', 'branch'], branch, options) 21 | execOptions = processResult: (err, stdout) -> gitUtil.parseBranches stdout unless hasName 22 | execute command, @_getOptions(execOptions), callback 23 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | grunt.initConfig 3 | pkg: grunt.file.readJSON('package.json') 4 | 5 | watch: 6 | coffee: 7 | files: 'src/*.coffee' 8 | tasks: ['test'] 9 | test: 10 | files: 'test/*.coffee' 11 | tasks: ['test'] 12 | 13 | coffee: 14 | dist: 15 | expand: true 16 | flatten: true 17 | cwd: 'src' 18 | src: ['*.coffee'] 19 | dest: 'lib/' 20 | ext: '.js' 21 | 22 | mochacov: 23 | options: 24 | compilers: ['coffee:coffee-script/register'] 25 | files: ['test/*.coffee'] 26 | coverage: 27 | options: 28 | coveralls: true 29 | test: 30 | options: 31 | reporter: 'spec' 32 | 33 | 34 | grunt.loadNpmTasks 'grunt-contrib-watch' 35 | grunt.loadNpmTasks 'grunt-contrib-coffee' 36 | grunt.loadNpmTasks 'grunt-mocha-cov' 37 | 38 | grunt.registerTask 'test', ['mochacov:test'] 39 | grunt.registerTask 'coverage', ['mochacov:coverage'] 40 | grunt.registerTask 'build', ['coffee:dist'] 41 | 42 | grunt.registerTask 'default', ['test', 'watch'] 43 | -------------------------------------------------------------------------------- /src/util.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | 3 | util = 4 | hasType: (object, type) -> 5 | object.constructor.name == type.name 6 | 7 | checkArgs: (object, allowedTypes) -> 8 | allowedTypes = [allowedTypes] unless _.isArray allowedTypes 9 | valid = false 10 | for type in allowedTypes 11 | break if valid = @hasType object, type 12 | unless valid 13 | allowedTypesString = _.map(allowedTypes, (t) -> t.name).join ', ' 14 | throw new TypeError("expected #{allowedTypesString} but got #{object.constructor.name}") 15 | true 16 | 17 | escape: (s, chars=['"', "'"], escapeChar="\\") -> 18 | regexp = new RegExp("([#{chars.join('')}])", 'g') 19 | s.replace regexp, "#{escapeChar}$1" 20 | 21 | quote: (value, escape=false) -> 22 | value = value.replace(/"/g, "\\\"") if escape 23 | "\"#{value}\"" 24 | 25 | quoteAll: (values, escape=false) -> 26 | _.map values, (value) => 27 | @quote value, escape 28 | 29 | setOptions: (options, callback) -> 30 | if _.isFunction(options) && !callback? 31 | [{}, options] 32 | else 33 | [options, callback ? null] 34 | 35 | module.exports = util 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-cli", 3 | "version": "0.11.0", 4 | "description": "Simple CLI like git interface for NodeJS", 5 | "main": "lib/git-cli.js", 6 | "scripts": { 7 | "test": "grunt test", 8 | "prepublish": "grunt build" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/tuvistavie/node-git-cli.git" 13 | }, 14 | "keywords": [ 15 | "git" 16 | ], 17 | "author": "Daniel Perez", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/tuvistavie/node-git-cli/issues" 21 | }, 22 | "homepage": "https://github.com/tuvistavie/node-git-cli", 23 | "devDependencies": { 24 | "coffee-script": "^1.7.1", 25 | "expect.js": "^0.3.1", 26 | "grunt": "^0.4.4", 27 | "grunt-cli": "^1.3.1", 28 | "grunt-contrib-coffee": "^0.10.1", 29 | "grunt-contrib-watch": "^0.6.1", 30 | "grunt-exec": "^0.4.5", 31 | "grunt-mocha-cov": "^0.4.0", 32 | "mocha": "^2.4.5", 33 | "tmp": "0.0.28" 34 | }, 35 | "dependencies": { 36 | "underscore": "^1.6.0", 37 | "fs-extra": "^0.9.1" 38 | }, 39 | "config": { 40 | "blanket": { 41 | "pattern": "src", 42 | "loader": "./node-loaders/coffee-script", 43 | "data-cover-never": "node_modules" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/index-actions.coffee: -------------------------------------------------------------------------------- 1 | util = require './util' 2 | _ = require 'underscore' 3 | CliCommand = require './cli-command' 4 | execute = require('./runner').execute 5 | 6 | exports.add = (files, options, callback) -> 7 | if _.isArray files 8 | [options, callback] = util.setOptions options, callback 9 | else 10 | [options, callback] = util.setOptions files, options 11 | files = ['.'] 12 | args = [] 13 | Array.prototype.push.apply(args, files) 14 | command = new CliCommand(['git', 'add'], args, options) 15 | execute command, @_getOptions(), callback 16 | 17 | exports.commit = (message, options, callback) -> 18 | [options, callback] = util.setOptions options, callback 19 | cliOpts = _.extend({m: message}, options) 20 | command = new CliCommand(['git', 'commit'], cliOpts) 21 | execute command, @_getOptions(), callback 22 | 23 | exports.checkout = (branch, options, callback) -> 24 | [options, callback] = util.setOptions options, callback 25 | command = new CliCommand(['git', 'checkout'], branch, options) 26 | execute command, @_getOptions(), callback 27 | 28 | exports.merge = (args, options, callback) -> 29 | [options, callback] = util.setOptions options, callback 30 | command = new CliCommand(['git', 'merge'], args, options) 31 | execute command, @_getOptions(), callback 32 | -------------------------------------------------------------------------------- /src/remote.coffee: -------------------------------------------------------------------------------- 1 | CliCommand = require './cli-command' 2 | gitUtil = require './git-util' 3 | util = require './util' 4 | execute = require('./runner').execute 5 | 6 | exports.listRemotes = (options, callback) -> 7 | [options, callback] = util.setOptions options, callback 8 | command = new CliCommand(['git', 'remote', 'show'], options) 9 | execOptions = processResult: (err, stdout) -> stdout.trim().split "\n" 10 | execute command, @_getOptions(execOptions), callback 11 | 12 | exports.showRemote = (name, options, callback) -> 13 | [options, callback] = util.setOptions options, callback 14 | command = new CliCommand(['git', 'remote', 'show'], name, options) 15 | execOptions = processResult: (err, stdout) -> gitUtil.parseRemote stdout 16 | execute command, @_getOptions(execOptions), callback 17 | 18 | exports.addRemote = (name, url, options, callback) -> 19 | [options, callback] = util.setOptions options, callback 20 | command = new CliCommand(['git', 'remote', 'add'], [name, url], options) 21 | execute command, @_getOptions(), callback 22 | 23 | exports.setRemoteUrl = (name, url, options, callback) -> 24 | [options, callback] = util.setOptions options, callback 25 | command = new CliCommand(['git', 'remote', 'set-url'], [name, url], options) 26 | execute command, @_getOptions(), callback 27 | -------------------------------------------------------------------------------- /src/cli-command.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | 3 | Util = require './util' 4 | CliOption = require './cli-option' 5 | 6 | class CliCommand 7 | constructor: (@command, @args, options) -> 8 | unless options? || _.isString(@args) || _.isArray(@args) 9 | options = @args 10 | @args = undefined 11 | 12 | Util.checkArgs @command, [String, Array] 13 | Util.checkArgs @args, [String, Array] if @args? 14 | Util.checkArgs options, [Array, Object] if options? 15 | 16 | @command = [@command] unless _.isArray(@command) 17 | 18 | @args = [@args] if _.isString(@args) 19 | if options? 20 | options = _.pairs(options) unless _.isArray(options) 21 | @options = _.map options, ((opt) => @_initOption(opt)) 22 | 23 | _initOption: (option) -> 24 | Util.checkArgs option, [Array, CliOption] 25 | return option if Util.hasType(option, CliOption) 26 | if option.length != 2 27 | throw new TypeError("options object should be a single key/value pair") 28 | if _.isUndefined(option[1]) || option[1] == '' 29 | new CliOption(option[0]) 30 | else 31 | new CliOption(option[0], option[1]) 32 | 33 | toString: -> 34 | s = @command.join(' ') 35 | s += ' ' + _.map(@options, (opt) -> opt.toString()).join(' ') if @options? 36 | s += ' ' + @args.join(' ') if @args? 37 | s.trim() 38 | 39 | 40 | module.exports = CliCommand 41 | -------------------------------------------------------------------------------- /src/cli-option.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | 3 | Util = require './util' 4 | 5 | class CliOption 6 | constructor: (option, args) -> 7 | Util.checkArgs option, [Array, String, Object] 8 | if _.isUndefined(args) && _.isString(option) 9 | @option = option 10 | @hasArgs = false 11 | else 12 | @_initWithArguments option, args 13 | 14 | _initWithArguments: (option, args) -> 15 | if _.isUndefined args 16 | Util.checkArgs option, [Array, Object] 17 | option = if _.isArray(option) then [option] else _.pairs(option) 18 | if option.length != 1 19 | throw new TypeError("options object should be a single key/value pair") 20 | [@option, @args] = option[0] 21 | else 22 | [@option, @args] = [option, args] 23 | Util.checkArgs @args, [Array, String, Number, Boolean] 24 | @args = [@args] unless _.isArray @args 25 | @args = _.map @args, (a) -> if a == _.isBoolean(a) then '' else a.toString() 26 | 27 | @hasArgs = _.any @args, (a) -> a.length > 0 28 | 29 | toString: -> 30 | if @hasArgs 31 | @_formatOptionWithArgs() 32 | else 33 | @_formatSimpleOption() 34 | 35 | _formatSimpleOption: -> 36 | prefix = if @option.length == 1 then '-' else '--' 37 | prefix + @option 38 | 39 | _formatOptionWithArgs: -> 40 | argsString = Util.quoteAll(@args, true).join ' ' 41 | if @option.length == 1 42 | "-#{@option} #{argsString}" 43 | else 44 | "--#{@option}=#{argsString}" 45 | 46 | module.exports = CliOption 47 | -------------------------------------------------------------------------------- /test/cli-option-test.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | 3 | CliOption = require('../src/cli-option') 4 | 5 | describe 'CliOption', -> 6 | describe 'constructor', -> 7 | it 'should work without arguments', -> 8 | option = new CliOption('a') 9 | expect(option.hasArgs).to.be false 10 | 11 | it 'should work with arguments as object', -> 12 | option = new CliOption({a: '123'}) 13 | expect(option.hasArgs).to.be true 14 | expect(option.args).to.eql ['123'] 15 | 16 | it 'should work with arguments as array', -> 17 | option = new CliOption(['a', '123']) 18 | expect(option.hasArgs).to.be true 19 | expect(option.args).to.eql ['123'] 20 | 21 | it 'should work with second argument', -> 22 | option = new CliOption('a', '123') 23 | expect(option.hasArgs).to.be true 24 | expect(option.args).to.eql ['123'] 25 | 26 | describe '#toString', -> 27 | it 'should format short options wihout args', -> 28 | result = new CliOption('a').toString() 29 | expect(result).to.be '-a' 30 | 31 | it 'should format long options without args', -> 32 | result = new CliOption('all').toString() 33 | expect(result).to.be '--all' 34 | 35 | it 'should format short options with args', -> 36 | option = new CliOption('m', 'lorem ipsum') 37 | expected = "-m \"lorem ipsum\"" 38 | expect(option.toString()).to.be expected 39 | 40 | it 'should format long options with args', -> 41 | option = new CliOption('message', 'lorem ipsum') 42 | expected = "--message=\"lorem ipsum\"" 43 | expect(option.toString()).to.be expected 44 | 45 | it 'should ignore "true"', -> 46 | option = new CliOption('verbose', true) 47 | expected = "--verbose" 48 | expect(option.toString()).to.be expected 49 | -------------------------------------------------------------------------------- /src/stats.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | gitUtil = require './git-util' 3 | execute = require('./runner').execute 4 | util = require './util' 5 | CliCommand = require './cli-command' 6 | 7 | exports.diff = (options={}, callback) -> 8 | [options, callback] = util.setOptions options, callback 9 | args = @_getDiffArgs(options) 10 | command = new CliCommand(['git', 'diff'], args, options) 11 | execute command, @_getOptions(), callback 12 | 13 | exports.diffStats = (options={}, callback) -> 14 | [options, callback] = util.setOptions options, callback 15 | args = @_getDiffArgs(options) 16 | cliOpts = _.extend({ shortstat: '' }, options) 17 | command = new CliCommand(['git', 'diff'], args, cliOpts) 18 | execOptions = processResult: (err, stdout) -> gitUtil.parseShortDiff(stdout) 19 | execute command, @_getOptions(execOptions), callback 20 | 21 | exports._getDiffArgs = (options) -> 22 | args = [] 23 | args.push options.source if options.source? 24 | args.push options.target if options.target? 25 | if options.path? 26 | args.push '--' 27 | Array.prototype.push.apply args, options.paths 28 | args 29 | 30 | exports.status = (options, callback) -> 31 | [options, callback] = util.setOptions options, callback 32 | command = new CliCommand(['git', 'status'], _.extend({ s: '' }, options)) 33 | execOptions = 34 | processResult: (err, stdout) => 35 | statusInfo = gitUtil.parseStatus(stdout) 36 | _.each(statusInfo, (f) => f.fullPath = "#{@workingDir()}/#{f.path}") 37 | execute command, @_getOptions(execOptions), callback 38 | 39 | exports.log = (options={}, callback) -> 40 | [options, callback] = util.setOptions options, callback 41 | format = '{"author": "%an", "email": "%ae", "date": "%cd", "subject": "%s", "body": "%b", "hash": "%H"},' 42 | cliOpts = _.extend({ pretty: "format:#{format}" }, options) 43 | command = new CliCommand(['git', 'log'], cliOpts) 44 | execOptions = processResult: (err, stdout) -> gitUtil.parseLog(stdout) 45 | execute command, @_getOptions(execOptions), callback 46 | -------------------------------------------------------------------------------- /src/git-util.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | 3 | GitUtil = 4 | parseStatus: (statusStr) -> 5 | files = [] 6 | 7 | for line in statusStr.split('\n') 8 | continue if line.trim() == '' 9 | [type, path] = [line.substring(0, 2), line.substring(3)] 10 | [type, tracked] = if type[0] == ' ' then [type[1], false] else [type[0], true] 11 | switch type 12 | when '?' then [status, tracked] = ['added', false] 13 | when 'M' then status = 'modified' 14 | when 'A' then status = 'added' 15 | when 'D' then status = 'removed' 16 | files.push 17 | path: path 18 | status: status 19 | tracked: tracked 20 | 21 | files 22 | 23 | parseShortDiff: (diffStr) -> 24 | diffStr = diffStr.trim() 25 | regexp = /(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/ 26 | result = regexp.exec diffStr 27 | 28 | if result? 29 | stats = _.map result[1..], (v) -> (if v then parseInt(v, 10) else 0) 30 | else 31 | stats = [0, 0, 0] 32 | 33 | { 34 | changedFilesNumber: stats[0] 35 | insertions: stats[1] 36 | deletions: stats[2] 37 | } 38 | 39 | parseLog: (logStr) -> 40 | logStr = logStr.replace /"},\n/g, '"},' # remove linebreaks between commits 41 | logStr = logStr.replace /\n/g, '\\n' # replace linebreaks in messages 42 | logStr = '[' + logStr[0...-1] + ']' # remove last comma 43 | logs = JSON.parse logStr 44 | _.each logs, (log) -> 45 | log.date = new Date(Date.parse(log.date)) 46 | logs 47 | 48 | parseRemote: (remoteStr) -> 49 | remoteStr = remoteStr.trim() 50 | 51 | fetchUrl: /\s+Fetch URL: (.*?)\n/.exec(remoteStr)?[1] 52 | pushUrl: /\s+Push URL: (.*?)\n/.exec(remoteStr)?[1] 53 | headBranch: /\s+HEAD branch: (.*?)\n/.exec(remoteStr)?[1] 54 | 55 | parseCurrentBranch: (branches) -> 56 | branches = branches.trim().split '\n' 57 | branch = _.find branches, (b) -> b[0] == '*' 58 | if branch? then branch.substring(2) else undefined 59 | 60 | parseBranches: (branches) -> 61 | branches = branches.trimRight().split '\n' 62 | _.map branches, (b) -> b.substring(2) 63 | 64 | 65 | module.exports = GitUtil 66 | -------------------------------------------------------------------------------- /test/cli-command-test.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | 3 | CliOption = require('../src/cli-option') 4 | CliCommand = require('../src/cli-command') 5 | 6 | describe 'CliCommand', -> 7 | describe 'constructor', -> 8 | it 'should work with no arguments or options', -> 9 | command = new CliCommand('foo') 10 | expect(command.args).to.be undefined 11 | expect(command.options).to.be undefined 12 | 13 | it 'should work with no options', -> 14 | command = new CliCommand('foo', 'abc') 15 | expect(command.args).to.eql ['abc'] 16 | expect(command.options).to.be undefined 17 | 18 | command = new CliCommand('foo', ['abc', 'def']) 19 | expect(command.args).to.eql ['abc', 'def'] 20 | expect(command.options).to.be undefined 21 | 22 | it 'should work with no arguments', -> 23 | command = new CliCommand('foo', {abc: 123}) 24 | expect(command.args).to.be undefined 25 | expect(command.options).to.eql([new CliOption('abc', 123)]) 26 | 27 | it 'should work arguments and options', -> 28 | command = new CliCommand('foo', ['abc', 'def'], {foo: 123, bar: ''}) 29 | expect(command.args).to.eql ['abc', 'def'] 30 | expect(command.options).to.eql [new CliOption('foo', 123), new CliOption('bar')] 31 | 32 | describe '#toString', -> 33 | it 'should format command with no arguments or options', -> 34 | command = new CliCommand('ls') 35 | expect(command.toString()).to.be 'ls' 36 | 37 | it 'should format command with arguments', -> 38 | command = new CliCommand('git', ['add', '.']) 39 | expect(command.toString()).to.be 'git add .' 40 | 41 | it 'should format command with options', -> 42 | command = new CliCommand('pacman', { S: 'abc', verbose: '' }) 43 | expect(command.toString()).to.be "pacman -S \"abc\" --verbose" 44 | command = new CliCommand(['git', 'commit'], { m: 'my message', a: '' }) 45 | expect(command.toString()).to.be "git commit -m \"my message\" -a" 46 | 47 | it 'should format command with arguments and options', -> 48 | command = new CliCommand(['git', 'diff'], ['HEAD~5', 'HEAD', '--', 'README.md'], { s: '' }) 49 | expect(command.toString()).to.be "git diff -s HEAD~5 HEAD -- README.md" 50 | 51 | -------------------------------------------------------------------------------- /test/util-test.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | 3 | util = require '../src/util' 4 | 5 | describe 'util', -> 6 | describe 'hasType', -> 7 | it 'should return true on same type', -> 8 | expect(util.hasType('abc', String)).to.be true 9 | expect(util.hasType(1, Number)).to.be true 10 | expect(util.hasType([], Array)).to.be true 11 | expect(util.hasType({}, Object)).to.be true 12 | 13 | it 'should return false otherwie', -> 14 | expect(util.hasType('abc', Number)).to.be false 15 | expect(util.hasType([], Object)).to.be false 16 | 17 | describe 'checkArgs', -> 18 | it 'should return true when type matches', -> 19 | expect(util.checkArgs 'abc', String).to.be true 20 | 21 | it 'should return false when any type matches', -> 22 | expect(util.checkArgs 'abc', [Array, String]).to.be true 23 | 24 | it 'should throw otherwise', -> 25 | fn = (-> util.checkArgs 'abc', Array) 26 | expect(fn).to.throwException (e) -> 27 | expect(e).to.be.a TypeError 28 | 29 | fn = (-> util.checkArgs 'abc', [Array, Object]) 30 | expect(fn).to.throwException (e) -> 31 | expect(e).to.be.a TypeError 32 | 33 | describe 'quote', -> 34 | it 'should quote raw argument', -> 35 | expect(util.quote('foo')).to.be "\"foo\"" 36 | 37 | it 'should escape string quotes', -> 38 | expect(util.quote("foo\"", true)).to.be '"foo\\""' 39 | expect(util.quote("\"foo\"", true)).to.be '"\\"foo\\""' 40 | 41 | describe 'quoteAll', -> 42 | it 'should quote all elements', -> 43 | expect(util.quoteAll(["foo", "bar"])).to.eql ["\"foo\"", "\"bar\""] 44 | 45 | describe 'escape', -> 46 | it 'should escape quotes by default', -> 47 | s = "abc'def'ghi\"" 48 | expected = "abc\\'def\\'ghi\\\"" 49 | expect(util.escape(s)).to.eql expected 50 | 51 | it 'should escape given chars', -> 52 | s = "abcdefg'hi" 53 | expected = "abc\\def\\g'hi" 54 | expect(util.escape(s, ['d', 'g'])).to.eql expected 55 | 56 | describe 'setOptions', -> 57 | it 'should work without options', -> 58 | [options, callback] = util.setOptions (-> 1) 59 | expect(options).to.be.a('object') 60 | expect(callback).to.be.a('function') 61 | 62 | it 'should work with options and callback', -> 63 | [options, callback] = util.setOptions { force: true }, (-> 1) 64 | expect(options).to.be.a('object') 65 | expect(callback).to.be.a('function') 66 | expect(options.force).to.be true 67 | 68 | it 'should work with options and no callback', -> 69 | [options, callback] = util.setOptions { force: true } 70 | expect(options).to.be.a('object') 71 | expect(callback).to.be null 72 | expect(options.force).to.be true 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-git-cli [![Build Status][travis-img]][travis-build] [![Coverage Status][coveralls]][coveralls-img] 2 | 3 | A simple git interface for NodeJS. 4 | It is not intended to replace projects such as 5 | [nodegit](https://github.com/nodegit/nodegit) but 6 | rather to provide a light weight solution close to 7 | the git command line for simple use cases. 8 | 9 | ## Installation 10 | 11 | Just run 12 | 13 | ``` 14 | $ npm install git-cli 15 | ``` 16 | 17 | ## Usage 18 | 19 | The usage is pretty straightforward, here is a sample code. 20 | 21 | ```coffee 22 | Repository = require('git-cli').Repository 23 | fs = require 'fs' 24 | 25 | Repository.clone 'https://github.com/danhper/node-git-cli.git', 'git-cli', (err, repo) -> 26 | repo.log (err, logs) -> 27 | console.log logs[0].subject 28 | repo.showRemote 'origin', (err, remote) -> 29 | console.log remote.fetchUrl 30 | 31 | fs.writeFileSync "#{repo.workingDir()}/newFile", 'foobar' 32 | repo.status (err, status) -> 33 | console.log status[0].path 34 | console.log status[0].tracked 35 | 36 | repo.add (err) -> 37 | repo.status (err, status) -> 38 | console.log status[0].path 39 | console.log status[0].tracked 40 | 41 | repo.commit 'added newFile', (err) -> 42 | repo.log (err, logs) -> 43 | console.log logs[0].subject 44 | 45 | repo.push (err) -> 46 | console.log 'pushed to remote' 47 | ``` 48 | 49 | From version 0.10, all functions still take a callback, but also return promises, 50 | so you can rewrite the above as follow: 51 | 52 | ```javascript 53 | const Repository = require('git-cli').Repository 54 | const fs = require('fs') 55 | 56 | Repository.clone('https://github.com/danhper/node-git-cli.git', 'git-cli') 57 | .then(repo => { 58 | return repo.log() 59 | .then(logs => { 60 | console.log(logs[0].subject) 61 | return repo.showRemote('origin') 62 | }).then(remote => { 63 | console.log(remote.fetchUrl) 64 | fs.writeFileSync("#{repo.workingDir()}/newFile", 'foobar') 65 | return repo.status() 66 | }).then(status => { 67 | console.log(status[0].path) 68 | console.log(status[0].tracked) 69 | return repo.add() 70 | }).then(() => repo.status()) 71 | .then(status => { 72 | console.log status[0].path 73 | console.log status[0].tracked 74 | return repo.commit('added newFile') 75 | }).then(() => repo.log()) 76 | .then(logs => { 77 | console.log(logs[0].subject) 78 | return repo.push() 79 | }).then(() => console.log('pushed' to remote)) 80 | }).catch(e => console.log(e)) 81 | ``` 82 | 83 | Checkout out [the tests](test/repository-test.coffee) for more examples. 84 | 85 | [travis-build]: https://travis-ci.org/danhper/node-git-cli 86 | [travis-img]: https://travis-ci.org/danhper/node-git-cli.svg?branch=master 87 | [coveralls]: https://coveralls.io/repos/danhper/node-git-cli/badge.png?branch=master 88 | [coveralls-img]: https://coveralls.io/r/danhper/node-git-cli?branch=master 89 | -------------------------------------------------------------------------------- /test/git-util-test.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | expect = require 'expect.js' 3 | 4 | GitUtil = require '../src/git-util' 5 | 6 | describe 'GitUtil', -> 7 | describe '#parseStatus', -> 8 | it 'should parse tracked changes', -> 9 | s = """ 10 | D LICENSE 11 | M src/git-util.coffee 12 | ?? test/git-util-test.coffee 13 | """ 14 | changes = GitUtil.parseStatus(s) 15 | expect(changes.length).to.be 3 16 | expected = [ 17 | { status: 'removed', tracked: false, path: 'LICENSE' } 18 | { status: 'modified', tracked: false, path: 'src/git-util.coffee' } 19 | { status: 'added', tracked: false, path: 'test/git-util-test.coffee' } 20 | ] 21 | 22 | _.each expected, (expectedChanges, index) -> 23 | _.each expectedChanges, (v, k) -> 24 | expect(changes[index][k]).to.be v 25 | 26 | describe '#parseShortDiff', -> 27 | checkStats = (s, expected) -> 28 | stats = GitUtil.parseShortDiff(s) 29 | expect(stats).to.be.a Object 30 | _.each expected, (v, k) -> 31 | expect(stats).to.have.key k 32 | expect(stats[k]).to.be v 33 | 34 | 35 | it 'should parse singular', -> 36 | s = ' 1 file changed, 1 insertion(+), 1 deletion(-)\n' 37 | expected = { changedFilesNumber: 1, insertions: 1, deletions: 1} 38 | checkStats s, expected 39 | 40 | it 'should parse plural', -> 41 | s = ' 2 files changed, 3 insertions(+), 4 deletions(-)' 42 | expected = { changedFilesNumber: 2, insertions: 3, deletions: 4} 43 | checkStats s, expected 44 | 45 | it 'should work with only insertions', -> 46 | s = ' 1 file changed, 3 insertions(+)' 47 | expected = { changedFilesNumber: 1, insertions: 3, deletions: 0} 48 | checkStats s, expected 49 | 50 | it 'should work with only deletions', -> 51 | s = ' 2 files changed, 1 deletion(-)' 52 | expected = { changedFilesNumber: 2, insertions: 0, deletions: 1} 53 | checkStats s, expected 54 | 55 | describe '#parseRemote', -> 56 | it 'should parse remote info', -> 57 | s = """ 58 | * remote origin 59 | Fetch URL: git@github.com:tuvistavie/node-git-cli.git 60 | Push URL: git@github.com:tuvistavie/node-git-cli.git 61 | HEAD branch: master 62 | Remote branch: 63 | master 64 | Local branch configured for 'git pull': 65 | master merges with remote master 66 | Local ref configured for 'git push': 67 | master pushes to master (up to date) 68 | 69 | """ 70 | remoteInfo = GitUtil.parseRemote s 71 | expected = 72 | fetchUrl: 'git@github.com:tuvistavie/node-git-cli.git' 73 | pushUrl: 'git@github.com:tuvistavie/node-git-cli.git' 74 | headBranch: 'master' 75 | 76 | expect(remoteInfo).to.eql expected 77 | 78 | describe '#parseCurrentBranch', -> 79 | it 'should parse current branch', -> 80 | s = """ 81 | dev 82 | facebook_share 83 | master 84 | mobile 85 | * new_design 86 | 87 | """ 88 | currentBranch = GitUtil.parseCurrentBranch s 89 | expect(currentBranch).to.be 'new_design' 90 | 91 | it 'should return undefined when no current branch', -> 92 | expect(GitUtil.parseCurrentBranch('')).to.be undefined 93 | 94 | describe '#parseBranches', -> 95 | it 'should parse branch list', -> 96 | s = """ 97 | dev 98 | facebook_share 99 | master 100 | mobile 101 | * new_design 102 | 103 | """ 104 | branches = GitUtil.parseBranches s 105 | expected = ['dev', 'facebook_share', 'master', 'mobile', 'new_design'] 106 | expect(branches).to.eql expected 107 | -------------------------------------------------------------------------------- /test/repository-test.coffee: -------------------------------------------------------------------------------- 1 | process.env['TMPDIR'] = '/tmp/node-git-cli' 2 | 3 | _ = require 'underscore' 4 | expect = require 'expect.js' 5 | tmp = require 'tmp' 6 | fs = require 'fs-extra' 7 | 8 | Helpers = require './test-helpers' 9 | gitCli = require '../src/git-cli' 10 | Repository = require '../src/repository' 11 | CliOption = require '../src/cli-option' 12 | 13 | BASE_REPO_PATH = '/home/daniel/Documents/projects/node-git-cli' 14 | unless fs.existsSync BASE_REPO_PATH 15 | BASE_REPO_PATH = 'https://github.com/tuvistavie/node-git-cli.git' 16 | 17 | [baseRepository, testRepository] = [null, null] 18 | 19 | before () -> 20 | if fs.existsSync(process.env['TMPDIR']) 21 | fs.removeSync(process.env['TMPDIR']) 22 | fs.mkdirSync(process.env['TMPDIR']) 23 | path = tmp.dirSync().name 24 | Repository.clone(BASE_REPO_PATH, "#{path}/node-git-cli", { bare: true }) 25 | .then((repo) -> baseRepository = repo) 26 | 27 | after -> 28 | fs.removeSync(process.env['TMPDIR']) 29 | 30 | describe 'Repository', -> 31 | beforeEach () -> 32 | path = tmp.dirSync().name 33 | Repository.clone(baseRepository.workingDir(), "#{path}/node-git-cli") 34 | .then((repo) -> testRepository = repo) 35 | 36 | describe 'constructor', -> 37 | it 'should throw error on wrong path', -> 38 | fn = -> new Repository('/wrong/path') 39 | expect(fn).to.throwException (e) -> 40 | expect(e).to.be.a gitCli.BadRepositoryError 41 | 42 | it 'should set the path', -> 43 | path = '/path/to/.git' 44 | repository = new Repository(path) 45 | expect(repository.path).to.be path 46 | 47 | describe 'workingDir', -> 48 | it 'should return the repository working directory', -> 49 | repository = new Repository('/path/to/.git') 50 | expect(repository.workingDir()).to.be '/path/to' 51 | 52 | describe '#_getOptions', -> 53 | it 'should return "cwd"', -> 54 | repository = new Repository('/path/to/.git') 55 | expect(repository._getOptions()).to.eql { cwd: '/path/to' } 56 | 57 | describe 'clone', -> 58 | it 'should clone repository to given directory', (done) -> 59 | path = tmp.dirSync({unsafeCleanup: true}).name 60 | Repository.clone(testRepository.path, "#{path}/node-git-cli") 61 | .then((repo) -> 62 | expect(repo.path).to.eql "#{path}/node-git-cli/.git" 63 | Repository.clone(testRepository.path, "#{path}/node-git-cli")) 64 | .then(-> done(new Error('should not be able to clone in existing dir'))) 65 | .catch((e) -> done()) 66 | 67 | describe 'init', -> 68 | it 'should init a new repository to given directory', () -> 69 | path = tmp.dirSync().name 70 | Repository.init("#{path}/node-git-cli") 71 | .then((repo) -> expect(repo.path).to.eql "#{path}/node-git-cli/.git") 72 | 73 | describe '#status', -> 74 | it 'get file status', () -> 75 | addedFilePath = "#{testRepository.workingDir()}/foo" 76 | editedFilePath = "#{testRepository.workingDir()}/README.md" 77 | fs.openSync(addedFilePath, 'w') 78 | fs.appendFileSync(editedFilePath, 'foobar') 79 | testRepository.status().then (changes) -> 80 | expect(changes).to.be.an Array 81 | expect(changes.length).to.be 2 82 | _.each { path: 'README.md', fullPath: editedFilePath, status: 'modified', tracked: false }, (v, k) -> 83 | expect(changes[0][k]).to.be v 84 | _.each { path: 'foo', fullPath: addedFilePath, status: 'added', tracked: false }, (v, k) -> 85 | expect(changes[1][k]).to.be v 86 | 87 | describe '#add', -> 88 | it 'should add all files by default', () -> 89 | fs.openSync("#{testRepository.workingDir()}/foo", 'w') 90 | fs.appendFileSync("#{testRepository.workingDir()}/README.md", 'foobar') 91 | testRepository.add() 92 | .then(-> testRepository.status()) 93 | .then (changes) -> 94 | expect(changes.length).to.be 2 95 | _.each changes, (change) -> 96 | expect(change.tracked).to.be true 97 | 98 | it 'shoud add given files otherwise', () -> 99 | addedFilePath = "#{testRepository.workingDir()}/foo" 100 | fs.openSync(addedFilePath, 'w') 101 | fs.appendFileSync("#{testRepository.workingDir()}/README.md", 'foobar') 102 | testRepository.add([addedFilePath]) 103 | .then(-> testRepository.status()) 104 | .then (changes) -> 105 | expect(changes.length).to.be(2) 106 | expect(changes[0].tracked).to.be(false) 107 | expect(changes[1].tracked).to.be(true) 108 | 109 | describe '#diff', -> 110 | it 'should not return output when files are not changed', () -> 111 | testRepository.diff().then((output) -> expect(output).to.be.empty()) 112 | 113 | it 'should return output when files are changed', () -> 114 | fs.appendFileSync("#{testRepository.workingDir()}/README.md", 'foobar') 115 | testRepository.diff().then((output) -> expect(output).to.not.be.empty()) 116 | 117 | describe '#diffStats', -> 118 | it 'should return correct stats', () -> 119 | fs.appendFileSync("#{testRepository.workingDir()}/README.md", 'foobar') 120 | Helpers.removeFirstLine("#{testRepository.workingDir()}/LICENSE") 121 | testRepository.diffStats().then (stats) -> 122 | expect(stats).to.be.a Object 123 | _.each { changedFilesNumber: 2, insertions: 1, deletions: 1}, (v, k) -> 124 | expect(stats[k]).to.be v 125 | 126 | describe '#log', -> 127 | it 'should return logs', () -> 128 | testRepository.log().then (logs) -> 129 | expect(logs).to.be.an Array 130 | expect(logs).to.not.be.empty() 131 | keys = ['author', 'email', 'subject', 'body', 'date', 'hash'] 132 | expect(logs[0]).to.only.have.keys keys 133 | expect(logs[0].date).to.be.a Date 134 | 135 | it 'should accept options and return logs', () -> 136 | testRepository.log({ n: 1 }).then (logs) -> 137 | expect(logs).to.be.an Array 138 | expect(logs).to.have.length 1 139 | 140 | describe '#commit', -> 141 | it 'should work when files are added', () -> 142 | fs.appendFileSync("#{testRepository.workingDir()}/README.md", 'foobar') 143 | testRepository.log().then (logs) -> 144 | logsCount = logs.length 145 | testRepository.add() 146 | .then(-> testRepository.commit "foo'bar") 147 | .then(-> testRepository.log()) 148 | .then((logs) -> expect(logs.length).to.be (logsCount + 1)) 149 | 150 | describe '#listRemotes', -> 151 | it 'should list all remotes', () -> 152 | testRepository.listRemotes().then((remotes) -> expect(remotes).to.eql(['origin'])) 153 | 154 | describe '#showRemote', -> 155 | it 'should get remote info', () -> 156 | testRepository.showRemote('origin').then (info) -> 157 | expected = 158 | pushUrl: baseRepository.workingDir() 159 | fetchUrl: baseRepository.workingDir() 160 | headBranch: 'master' 161 | expect(info).to.eql expected 162 | 163 | describe '#currentBranch', -> 164 | it 'should return current branch', () -> 165 | testRepository.currentBranch().then((branch) -> expect(branch).to.be 'master') 166 | 167 | describe '#branch', -> 168 | it 'should list branches', () -> 169 | testRepository.branch().then((branches) -> expect(branches).to.eql ['master']) 170 | 171 | it 'should create new branches', () -> 172 | testRepository.branch('foo') 173 | .then(-> testRepository.branch()) 174 | .then((branches) -> expect(branches).to.eql ['foo', 'master']) 175 | 176 | it 'should delete branches', () -> 177 | testRepository.branch('foo') 178 | .then(-> testRepository.branch()) 179 | .then((branches) -> 180 | expect(branches).to.eql ['foo', 'master'] 181 | testRepository.branch('foo', { D: true })) 182 | .then(-> testRepository.branch()) 183 | .then((branches) -> expect(branches).to.eql ['master']) 184 | 185 | describe '#checkout', -> 186 | it 'should do basic branch checkout', () -> 187 | testRepository.currentBranch() 188 | .then((branch) -> 189 | expect(branch).to.be 'master' 190 | testRepository.branch('gh-pages')) 191 | .then(-> testRepository.checkout 'gh-pages') 192 | .then(-> testRepository.currentBranch()) 193 | .then((branch) -> expect(branch).to.be('gh-pages')) 194 | 195 | it 'should work with -b flag', () -> 196 | testRepository.checkout('foo', { b: true }) 197 | .then(-> testRepository.currentBranch()) 198 | .then((branch) -> expect(branch).to.be 'foo') 199 | 200 | describe '#push', -> 201 | it 'should push commits', () -> 202 | fs.appendFileSync("#{testRepository.workingDir()}/README.md", 'foobar') 203 | baseRepository.log() 204 | .then (logs) -> 205 | logsCount = logs.length 206 | testRepository.commit("foo'bar", { a: true }) 207 | .then(-> testRepository.push()) 208 | .then(-> baseRepository.log()) 209 | .then((logs) -> expect(logs.length).to.be logsCount + 1) 210 | 211 | describe '#pull', -> 212 | it 'should pull commits', () -> 213 | path = tmp.dirSync().name 214 | Promise.all([ 215 | Repository.clone(baseRepository.workingDir(), "#{path}/node-git-cli-other"), 216 | testRepository.log() 217 | ]).then (result) -> 218 | [repo, logs] = result 219 | logsCount = logs.length 220 | fs.appendFileSync("#{repo.workingDir()}/README.md", 'foobarbaz') 221 | repo.commit("foobarbaz", { a: true }) 222 | .then(-> repo.push()) 223 | .then(-> testRepository.pull()) 224 | .then(-> testRepository.log()) 225 | .then((logs) -> expect(logs.length).to.be logsCount + 1) 226 | 227 | describe '#addRemote', -> 228 | it 'should add new remote', () -> 229 | testRepository.addRemote('foo', baseRepository.path) 230 | .then(-> testRepository.listRemotes()) 231 | .then((remotes) -> expect(remotes).to.contain('foo')) 232 | 233 | describe '#setRemoteUrl', -> 234 | it 'should change remote URL', () -> 235 | testRepository.setRemoteUrl('origin', 'newUrl') 236 | .then(-> testRepository.showRemote('origin', { n: true })) 237 | .then((remote) -> expect(remote.pushUrl).to.be('newUrl')) 238 | 239 | describe '#merge', -> 240 | it 'should merge branche', () -> 241 | file = "#{testRepository.workingDir()}/README.md" 242 | fs.appendFileSync(file, 'random string') 243 | testRepository.branch('newbranch') 244 | .then(-> testRepository.add()) 245 | .then(-> testRepository.commit("new commit")) 246 | .then(-> testRepository.checkout('newbranch')) 247 | .then(-> 248 | expect(fs.readFileSync(file, 'utf8')).to.not.contain 'random string' 249 | testRepository.merge('master')) 250 | .then(-> 251 | expect(fs.readFileSync(file, 'utf8')).to.contain 'random string' 252 | testRepository.log()) 253 | .then((logs) -> expect(logs[0].subject).to.be('new commit')) 254 | 255 | describe 'usage with callbacks', -> 256 | it 'should accept a callback', (done) -> 257 | testRepository.log (err, logs) -> 258 | expect(err).to.be.null 259 | expect(logs).to.be.an Array 260 | expect(logs).to.not.be.empty() 261 | keys = ['author', 'email', 'subject', 'body', 'date', 'hash'] 262 | expect(logs[0]).to.only.have.keys keys 263 | expect(logs[0].date).to.be.a Date 264 | done() 265 | --------------------------------------------------------------------------------