Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create new submodule update target to run git submodule update #61

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
101 changes: 101 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -704,5 +704,106 @@ Default value: none.

Without an optional `path` parameter, all files and subdirectories of the current working directory are included in the archive. If one or more paths are specified, only these are included.



## The "gitsubmoduleupdate" task

Updates submodules in the repository (via git submodule update).

### Overview
In your project's Gruntfile, add a section named `gitsubmoduleupdate` to the data object passed into `grunt.initConfig()`.

```js
grunt.initConfig({
gitsubmoduleupdate: {
your_target: {
options: {
// Target-specific options go here.
}
}
}
})
```

### Options

More detailed descriptions of these options are available at http://git-scm.com/docs/git-submodule.

#### options.init
Type: `boolean`
Default value: `false`

Initialize all submodules for which "git submodule init" has not been called so far before updating.

#### options.remote
Type: `Boolean`
Default value: `false`

Instead of using the superproject's recorded SHA-1 to update the submodule, use the status of the submodule's remote-tracking branch.

#### options.force
Type: `Boolean`
Default value: `false`

Throw away local changes in submodules when switching to a different commit.

#### options.rebase
Type: `Boolean`
Default value: `false`

Rebase the current branch onto the commit recorded in the superproject.

#### options.merge
Type: `Boolean`
Default value: `false`

Merge the commit recorded in the superproject into the current branch of the submodule.

#### options.reference
Type: `String`
Default value: `null`

This option will be passed to the ``git-clone()`` command used during an *update --init*

#### options.recursive
Type: `Boolean`
Default value: `false`

Traverse submodules recursively.

#### options.depth
Type: `Integer`
Default value: `null`

Create a 'shallow' clone with a history truncated to the specified number of revisions.

#### options.path
Type: `String`
Default value: `null`

path to the submodule to be updated. All submodules will be updated if path is not specified.

#### options.noFetch
Type: `Boolean`
Default value: `false`

Don't fetch new objects from the remote site.

### Usage Examples

```js
grunt.initConfig({
gitsubmoduleupdate: {
task: {
options: {
init: true,
recursive: true
}
}
}
});
```


## Contributing
In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [Grunt](http://gruntjs.com/).
56 changes: 56 additions & 0 deletions lib/command_submodule_update.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use strict';

var async = require('grunt').util.async;
var grunt = require('grunt');
var _s = require('underscore.string');
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the underscore string library seems like a more maintainable solution than inserting our own regex into each of the lib/command_* files. Underscore.string provides a method called dasherize that converts cammelCase strings to dash-separated strings. https://github.com/epeli/underscore.string.

Here is the regex that they use:

return _s.trim(str).replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();

This will convert noFetch to no-fetch, but would convert NoFetch to -no-fetch.

An alternative approach would be to create our own lib/util.js that contains a method to convert camelCase to dashes. From what I've read, the most efficient algorithm would be the following:

replace(/([a-z\d][A-Z])/g, function (g) { return g[0] + '-' + g[1].toLowerCase() });

See http://jsperf.com/js-camelcase for a comparison of regex performance going from a space-delimited string to camelCase. Using a sigle regex with a callback function was about 20% more efficient than chaining two replace methods.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: the jshint test now fails due to the variable starting with an underscore. Would you prefer to modify the jshint config, or modify the variable name? _s is the standard name for the underscore.string utility.


module.exports = function (task, exec, done) {
var optionKey;
var allowedOptions = {
init: false,
remote: false,
force: false,
rebase: false,
merge: false,
reference: null,
recursive: false,
noFetch: false
};

var options = task.options(allowedOptions);

var args = ['submodule', 'update'];


// Loop through allowable cli flags in options and add to args
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where I feel like I've done things differently than you would have, @rubenv ;-)

If you prefer, I am happy to change this to explicitly pushing individual args onto the args array, rather than doing so in a loop.

Notice that we are looping through allowedOptions, not options. This will ensure that only the supported options are added to the args array.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's cool, it cuts back on the amount of lines. Might be a good future improvement to convert all binary options everywhere to a logic similar to this.

We'd need to handle dashes: no-merge -> noMerge.

for (optionKey in allowedOptions) {
if (options.hasOwnProperty(optionKey) && options[optionKey]) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasOwnProperty probably isn't needed. Unless I'm mistaken, the result from options() is a plain object anyway.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I had to do a little research to confirm, but it looks like my understanding of property enumeration was very outdated. After ES5, it is quite simple to mark prototype properties as non-enumerable, so adding hasOwnProperty to every for-in loop is no longer necessary.

https://groups.google.com/forum/#!topic/jsmentors/2kmsNxOirFk

// Add flag
args.push('--' + _s.dasherize(optionKey));
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dasherize uses the following regex:

return _s.trim(str).replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();

//args.push('--' + optionKey);
// If not a boolean, add the value after the flag
if (typeof options[optionKey] !== 'boolean') {
args.push(options[optionKey]);
}
}
}

// If a path was specified, add it now:
if (options.path) {
args.push(options.path);
}

// Depth also needs to be specified explicitly here
// because a depth of zero would be skipped in the loop
if (options.depth !== null && options.depth !== undefined) {
args.push('--depth');
args.push(options.depth);
}

// Add callback
args.push(done);

exec.apply(this, args);
};

module.exports.description = 'Update git submodules.';
3 changes: 2 additions & 1 deletion lib/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ module.exports = {
rebase: require('./command_rebase'),
reset: require('./command_reset'),
stash: require('./command_stash'),
tag: require('./command_tag')
tag: require('./command_tag'),
submoduleupdate: require('./command_submodule_update')
};
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,8 @@
},
"keywords": [
"gruntplugin"
]
],
"dependencies": {
"underscore.string": "~2.3.3"
}
}
115 changes: 115 additions & 0 deletions test/submodule_update_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
'use strict';

var command = require('../lib/commands').submoduleupdate;
var Test = require('./_common');

describe('submodule update', function () {
it('should update submodules', function (done) {
var options = {
};

new Test(command, options)
.expect(['submodule', 'update'])
.run(done);
});

it('should accept init option', function (done) {
var options = {
init: true
};

new Test(command, options)
.expect(['submodule', 'update', '--init'])
.run(done);
});

it('should accept remote option', function (done) {
var options = {
remote: true
};

new Test(command, options)
.expect(['submodule', 'update', '--remote'])
.run(done);
});

it('should accept no-fetch option', function (done) {
var options = {
noFetch: true
};

new Test(command, options)
.expect(['submodule', 'update', '--no-fetch'])
.run(done);
});

it('should accept force option', function (done) {
var options = {
force: true
};

new Test(command, options)
.expect(['submodule', 'update', '--force'])
.run(done);
});

it('should accept rebase option', function (done) {
var options = {
rebase: true
};

new Test(command, options)
.expect(['submodule', 'update', '--rebase'])
.run(done);
});

it('should accept merge option', function (done) {
var options = {
merge: true
};

new Test(command, options)
.expect(['submodule', 'update', '--merge'])
.run(done);
});

it('should accept reference option', function (done) {
var options = {
reference: 'https://myrepo.com/repo.git'
};

new Test(command, options)
.expect(['submodule', 'update', '--reference', 'https://myrepo.com/repo.git'])
.run(done);
});

it('should accept depth option', function (done) {
var options = {
depth: 10
};

new Test(command, options)
.expect(['submodule', 'update', '--depth', '10'])
.run(done);
});

it('should accept recursive option', function (done) {
var options = {
recursive: true
};

new Test(command, options)
.expect(['submodule', 'update', '--recursive'])
.run(done);
});

it('should accept path option', function (done) {
var options = {
path: '/test/path'
};

new Test(command, options)
.expect(['submodule', 'update', '/test/path'])
.run(done);
});
});