diff --git a/config/default.yml b/config/default.yml index 9c808f68..1e502d40 100644 --- a/config/default.yml +++ b/config/default.yml @@ -430,6 +430,12 @@ PostCheckout: description: 'Generating tags file from source' required_executable: 'ctags' + SubmoduleStatus: + enabled: false + description: 'Checking submodule status' + quiet: true + recursive: false + # Hooks that run after a commit is created. PostCommit: ALL: @@ -450,6 +456,12 @@ PostCommit: description: 'Generating tags file from source' required_executable: 'ctags' + SubmoduleStatus: + enabled: false + description: 'Checking submodule status' + quiet: true + recursive: false + # Hooks that run after `git merge` executes successfully (no merge conflicts). PostMerge: ALL: @@ -461,6 +473,12 @@ PostMerge: description: 'Generating tags file from source' required_executable: 'ctags' + SubmoduleStatus: + enabled: false + description: 'Checking submodule status' + quiet: true + recursive: false + # Hooks that run after a commit is modified by an amend or rebase. PostRewrite: ALL: @@ -472,6 +490,12 @@ PostRewrite: description: 'Generating tags file from source' required_executable: 'ctags' + SubmoduleStatus: + enabled: false + description: 'Checking submodule status' + quiet: true + recursive: false + # Hooks that run during `git push`, after remote refs have been updated but # before any objects have been transferred. PrePush: diff --git a/lib/overcommit/git_repo.rb b/lib/overcommit/git_repo.rb index eb2e7e95..524cd7cc 100644 --- a/lib/overcommit/git_repo.rb +++ b/lib/overcommit/git_repo.rb @@ -11,6 +11,50 @@ module GitRepo \s@@.*$ /x + # Regular expression used to extract information from lines of + # `git submodule status` output + SUBMODULE_STATUS_REGEX = / + ^\s*(?[-+U]?)(?\w+) + \s(?[^\s]+?) + (?:\s\((?.+)\))?$ + /x + + # Struct encapsulating submodule information extracted from the + # output of `git submodule status` + SubmoduleStatus = Struct.new(:prefix, :sha1, :path, :describe) do + # Returns whether the submodule has not been initialized + def uninitialized? + prefix == '-' + end + + # Returns whether the submodule is out of date with the current + # index, i.e. its checked-out commit differs from that stored in + # the index of the parent repo + def outdated? + prefix == '+' + end + + # Returns whether the submodule reference has a merge conflict + def merge_conflict? + prefix == 'U' + end + end + + # Returns a list of SubmoduleStatus objects, one for each submodule in the + # parent repository. + # + # @option options [Boolean] recursive check submodules recursively + # @return [Array] + def submodule_statuses(options = {}) + flags = '--recursive' if options[:recursive] + + `git submodule status #{flags}`. + scan(SUBMODULE_STATUS_REGEX). + map do |prefix, sha1, path, describe| + SubmoduleStatus.new(prefix, sha1, path, describe) + end + end + # Extract the set of modified lines from a given file. # # @param file_path [String] diff --git a/lib/overcommit/hook/post_checkout/submodule_status.rb b/lib/overcommit/hook/post_checkout/submodule_status.rb new file mode 100644 index 00000000..54648216 --- /dev/null +++ b/lib/overcommit/hook/post_checkout/submodule_status.rb @@ -0,0 +1,30 @@ +module Overcommit::Hook::PostCheckout + # Checks the status of submodules in the current repository and + # notifies the user if any are uninitialized, out of date with + # the current index, or contain merge conflicts. + class SubmoduleStatus < Base + def run + messages = [] + submodule_statuses.each do |submodule_status| + path = submodule_status.path + if submodule_status.uninitialized? + messages << "Submodule #{path} is uninitialized." + elsif submodule_status.outdated? + messages << "Submodule #{path} is out of date with the current index." + elsif submodule_status.merge_conflict? + messages << "Submodule #{path} has merge conflicts." + end + end + + return :pass if messages.empty? + + [:warn, messages.join("\n")] + end + + private + + def submodule_statuses + Overcommit::GitRepo.submodule_statuses(recursive: config['recursive']) + end + end +end diff --git a/lib/overcommit/hook/post_commit/submodule_status.rb b/lib/overcommit/hook/post_commit/submodule_status.rb new file mode 100644 index 00000000..1648768a --- /dev/null +++ b/lib/overcommit/hook/post_commit/submodule_status.rb @@ -0,0 +1,30 @@ +module Overcommit::Hook::PostCommit + # Checks the status of submodules in the current repository and + # notifies the user if any are uninitialized, out of date with + # the current index, or contain merge conflicts. + class SubmoduleStatus < Base + def run + messages = [] + submodule_statuses.each do |submodule_status| + path = submodule_status.path + if submodule_status.uninitialized? + messages << "Submodule #{path} is uninitialized." + elsif submodule_status.outdated? + messages << "Submodule #{path} is out of date with the current index." + elsif submodule_status.merge_conflict? + messages << "Submodule #{path} has merge conflicts." + end + end + + return :pass if messages.empty? + + [:warn, messages.join("\n")] + end + + private + + def submodule_statuses + Overcommit::GitRepo.submodule_statuses(recursive: config['recursive']) + end + end +end diff --git a/lib/overcommit/hook/post_merge/submodule_status.rb b/lib/overcommit/hook/post_merge/submodule_status.rb new file mode 100644 index 00000000..979327ff --- /dev/null +++ b/lib/overcommit/hook/post_merge/submodule_status.rb @@ -0,0 +1,30 @@ +module Overcommit::Hook::PostMerge + # Checks the status of submodules in the current repository and + # notifies the user if any are uninitialized, out of date with + # the current index, or contain merge conflicts. + class SubmoduleStatus < Base + def run + messages = [] + submodule_statuses.each do |submodule_status| + path = submodule_status.path + if submodule_status.uninitialized? + messages << "Submodule #{path} is uninitialized." + elsif submodule_status.outdated? + messages << "Submodule #{path} is out of date with the current index." + elsif submodule_status.merge_conflict? + messages << "Submodule #{path} has merge conflicts." + end + end + + return :pass if messages.empty? + + [:warn, messages.join("\n")] + end + + private + + def submodule_statuses + Overcommit::GitRepo.submodule_statuses(recursive: config['recursive']) + end + end +end diff --git a/lib/overcommit/hook/post_rewrite/submodule_status.rb b/lib/overcommit/hook/post_rewrite/submodule_status.rb new file mode 100644 index 00000000..8e79a16f --- /dev/null +++ b/lib/overcommit/hook/post_rewrite/submodule_status.rb @@ -0,0 +1,30 @@ +module Overcommit::Hook::PostRewrite + # Checks the status of submodules in the current repository and + # notifies the user if any are uninitialized, out of date with + # the current index, or contain merge conflicts. + class SubmoduleStatus < Base + def run + messages = [] + submodule_statuses.each do |submodule_status| + path = submodule_status.path + if submodule_status.uninitialized? + messages << "Submodule #{path} is uninitialized." + elsif submodule_status.outdated? + messages << "Submodule #{path} is out of date with the current index." + elsif submodule_status.merge_conflict? + messages << "Submodule #{path} has merge conflicts." + end + end + + return :pass if messages.empty? + + [:warn, messages.join("\n")] + end + + private + + def submodule_statuses + Overcommit::GitRepo.submodule_statuses(recursive: config['recursive']) + end + end +end diff --git a/spec/overcommit/hook/post_checkout/submodule_status_spec.rb b/spec/overcommit/hook/post_checkout/submodule_status_spec.rb new file mode 100644 index 00000000..45078f46 --- /dev/null +++ b/spec/overcommit/hook/post_checkout/submodule_status_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Overcommit::Hook::PostCheckout::SubmoduleStatus do + let(:config) { Overcommit::ConfigurationLoader.default_configuration } + let(:context) { double('context') } + subject { described_class.new(config, context) } + + let(:submodule_status) { double('submodule_status') } + + before do + submodule_status.stub(:path).and_return('sub') + subject.stub(:submodule_statuses).and_return([submodule_status]) + end + + context 'when submodule is up to date' do + before do + submodule_status.stub(uninitialized?: false, + outdated?: false, + merge_conflict?: false) + end + + it { should pass } + end + + context 'when submodule is uninitialized' do + before do + submodule_status.stub(uninitialized?: true, + outdated?: false, + merge_conflict?: false) + end + + it { should warn(/uninitialized/) } + end + + context 'when submodule is outdated' do + before do + submodule_status.stub(uninitialized?: false, + outdated?: true, + merge_conflict?: false) + end + + it { should warn(/out of date/) } + end + + context 'when submodule has merge conflicts' do + before do + submodule_status.stub(uninitialized?: false, + outdated?: false, + merge_conflict?: true) + end + + it { should warn(/merge conflicts/) } + end +end diff --git a/spec/overcommit/hook/post_commit/submodule_status_spec.rb b/spec/overcommit/hook/post_commit/submodule_status_spec.rb new file mode 100644 index 00000000..2c12d5fe --- /dev/null +++ b/spec/overcommit/hook/post_commit/submodule_status_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Overcommit::Hook::PostCommit::SubmoduleStatus do + let(:config) { Overcommit::ConfigurationLoader.default_configuration } + let(:context) { double('context') } + subject { described_class.new(config, context) } + + let(:submodule_status) { double('submodule_status') } + + before do + submodule_status.stub(:path).and_return('sub') + subject.stub(:submodule_statuses).and_return([submodule_status]) + end + + context 'when submodule is up to date' do + before do + submodule_status.stub(uninitialized?: false, + outdated?: false, + merge_conflict?: false) + end + + it { should pass } + end + + context 'when submodule is uninitialized' do + before do + submodule_status.stub(uninitialized?: true, + outdated?: false, + merge_conflict?: false) + end + + it { should warn(/uninitialized/) } + end + + context 'when submodule is outdated' do + before do + submodule_status.stub(uninitialized?: false, + outdated?: true, + merge_conflict?: false) + end + + it { should warn(/out of date/) } + end + + context 'when submodule has merge conflicts' do + before do + submodule_status.stub(uninitialized?: false, + outdated?: false, + merge_conflict?: true) + end + + it { should warn(/merge conflicts/) } + end +end diff --git a/spec/overcommit/hook/post_merge/submodule_status_spec.rb b/spec/overcommit/hook/post_merge/submodule_status_spec.rb new file mode 100644 index 00000000..245fe6c1 --- /dev/null +++ b/spec/overcommit/hook/post_merge/submodule_status_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Overcommit::Hook::PostMerge::SubmoduleStatus do + let(:config) { Overcommit::ConfigurationLoader.default_configuration } + let(:context) { double('context') } + subject { described_class.new(config, context) } + + let(:submodule_status) { double('submodule_status') } + + before do + submodule_status.stub(:path).and_return('sub') + subject.stub(:submodule_statuses).and_return([submodule_status]) + end + + context 'when submodule is up to date' do + before do + submodule_status.stub(uninitialized?: false, + outdated?: false, + merge_conflict?: false) + end + + it { should pass } + end + + context 'when submodule is uninitialized' do + before do + submodule_status.stub(uninitialized?: true, + outdated?: false, + merge_conflict?: false) + end + + it { should warn(/uninitialized/) } + end + + context 'when submodule is outdated' do + before do + submodule_status.stub(uninitialized?: false, + outdated?: true, + merge_conflict?: false) + end + + it { should warn(/out of date/) } + end + + context 'when submodule has merge conflicts' do + before do + submodule_status.stub(uninitialized?: false, + outdated?: false, + merge_conflict?: true) + end + + it { should warn(/merge conflicts/) } + end +end diff --git a/spec/overcommit/hook/post_rewrite/submodule_status_spec.rb b/spec/overcommit/hook/post_rewrite/submodule_status_spec.rb new file mode 100644 index 00000000..c783c029 --- /dev/null +++ b/spec/overcommit/hook/post_rewrite/submodule_status_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Overcommit::Hook::PostRewrite::SubmoduleStatus do + let(:config) { Overcommit::ConfigurationLoader.default_configuration } + let(:context) { double('context') } + subject { described_class.new(config, context) } + + let(:submodule_status) { double('submodule_status') } + + before do + submodule_status.stub(:path).and_return('sub') + subject.stub(:submodule_statuses).and_return([submodule_status]) + end + + context 'when submodule is up to date' do + before do + submodule_status.stub(uninitialized?: false, + outdated?: false, + merge_conflict?: false) + end + + it { should pass } + end + + context 'when submodule is uninitialized' do + before do + submodule_status.stub(uninitialized?: true, + outdated?: false, + merge_conflict?: false) + end + + it { should warn(/uninitialized/) } + end + + context 'when submodule is outdated' do + before do + submodule_status.stub(uninitialized?: false, + outdated?: true, + merge_conflict?: false) + end + + it { should warn(/out of date/) } + end + + context 'when submodule has merge conflicts' do + before do + submodule_status.stub(uninitialized?: false, + outdated?: false, + merge_conflict?: true) + end + + it { should warn(/merge conflicts/) } + end +end