diff --git a/README.md b/README.md index ad3bca0b..4dd58026 100644 --- a/README.md +++ b/README.md @@ -174,11 +174,34 @@ vcsrepo { '/path/to/repo': } ~~~ -To keep the repository at the latest revision, set `ensure` to 'latest'. **Note**: `keep_local_changes` works by stashing local changes, switching the repo to the assigned revision and, finally, unstashing the local changes. It only comes into effect if the revision parameter is different from the local repo. This parameter DOES NOT delete/purge local changes by default on every run. -**WARNING:** This overwrites any local changes to the repository. +**WARNING:** This overwrites any conflicting local changes to the repository. + +To remove all un-committed changes in the local repository and submodules, set `repository_status` to `default_clean`. + +~~~ puppet +vcsrepo { '/path/to/repo': + ensure => present, + provider => git, + source => 'git://example.com/repo.git', + repository_status => 'default_clean', +} +~~~ + +The `default_clean` value directs vcsrepo to run commands necessary to ensure +that the status as reported by `git status` will not report any local changes. +This does not affect files specified in the `.gitignore` file; future versions +of vcsrepo may support more agressive cleaning if necessary, but this will not +be default. Note that when this parameter is is in use, the +`keep_local_changes` parameter has no net effect. + +This parameter respects the `submodules` option; when submodules are enabled, +the `default_clean` value will cause submodules to be cleaned as well and reset +to the commit specified by the containing repo. + +To keep the repository at the latest revision, set `ensure` to 'latest': ~~~ puppet vcsrepo { '/path/to/repo': @@ -796,7 +819,7 @@ For information on the classes and types, see the [REFERENCE.md](https://github. ##### `git` - Supports the Git VCS. -Features: `bare_repositories`, `depth`, `multiple_remotes`, `reference_tracking`, `ssh_identity`, `submodules`, `user` +Features: `bare_repositories`, `depth`, `multiple_remotes`, `reference_tracking`, `ssh_identity`, `submodules`, `user`, `working_copy_status' Parameters: `depth`, `ensure`, `excludes`, `force`, `group`, `identity`, `owner`, `path`, `provider`, `remote`, `revision`, `source`, `user` @@ -837,8 +860,8 @@ Parameters: `basic_auth_password`, `basic_auth_username`, `configuration`, `conf * `bare_repositories` - Differentiates between bare repositories and those with working copies. (Available with `git`.) * `basic_auth` - Supports HTTP Basic authentication. (Available with `hg` and `svn`.) -* `conflict` - Lets you decide how to resolve any conflicts between the source repository and your working copy. (Available with `svn`.) * `configuration` - Lets you specify the location of your configuration files. (Available with `svn`.) +* `conflict` - Lets you decide how to resolve any conflicts between the source repository and your working copy. (Available with `svn`.) * `cvs_rsh` - Understands the `CVS_RSH` environment variable. (Available with `cvs`.) * `depth` - Supports shallow clones in `git` or sets the scope limit in `svn`. (Available with `git` and `svn`.) * `filesystem_types` - Supports multiple types of filesystem. (Available with `svn`.) @@ -846,11 +869,12 @@ Parameters: `basic_auth_password`, `basic_auth_username`, `configuration`, `conf * `include_paths` - Lets you checkout only certain paths. (Available with `svn`.) * `modules` - Lets you choose a specific repository module. (Available with `cvs`.) * `multiple_remotes` - Tracks multiple remote repositories. (Available with `git`.) +* `p4config` - Supports setting the `P4CONFIG` environment. (Available with `p4`.) * `reference_tracking` - Lets you track revision references that can change over time (e.g., some VCS tags and branch names). (Available with all providers) * `ssh_identity` - Lets you specify an SSH identity file. (Available with `git` and `hg`.) -* `user` - Can run as a different user. (Available with `git`, `hg` and `cvs`.) -* `p4config` - Supports setting the `P4CONFIG` environment. (Available with `p4`.) * `submodules` - Supports repository submodules which can be optionally initialized. (Available with `git`.) +* `user` - Can run as a different user. (Available with `git`, `hg` and `cvs`.) +* `working_copy_status` - Can enforce the status of a working copy. (Available with `git`.) ## Limitations diff --git a/lib/puppet/provider/vcsrepo/git.rb b/lib/puppet/provider/vcsrepo/git.rb index 306013d3..8aa04d15 100644 --- a/lib/puppet/provider/vcsrepo/git.rb +++ b/lib/puppet/provider/vcsrepo/git.rb @@ -7,7 +7,7 @@ has_features :bare_repositories, :reference_tracking, :ssh_identity, :multiple_remotes, :user, :depth, :branch, :submodules, :safe_directory, :hooks_allowed, - :umask, :http_proxy, :tmpdir + :umask, :http_proxy, :tmpdir, :repository_status def create check_force @@ -72,6 +72,8 @@ def revision # @param [String] desired The desired revision to which the repo should be # set. def revision=(desired) + # Set the working copy status first + set_repository_status(@resource.value(:repository_status)) # just checkout tags and shas; fetch has already happened so they should be updated. checkout(desired) # branches require more work. @@ -232,6 +234,52 @@ def update_references end end + # Return the status of the working copy. + def repository_status + # Optimization: if we don't care about the status, then return right away. + # This avoids running 'git status', which may be costly on very large repos + # on slow, uncached filesystems. + if @resource.value(:repository_status) == :ignore + return :ignore + end + + at_path do + # 'git status' ignores files specified in .gitignore. + status = if @resource.value(:submodules) == :true + exec_git('status', '--porcelain') + else + exec_git('status', '--porcelain', '--ignore-submodules') + end + + return :default_clean if status.empty? + return :default_dirty + end + end + + def repository_status=(desired) + set_repository_status(desired) + end + + def set_repository_status(desired) + case desired + when :default_clean + at_path do + exec_git('clean', '-fd') + exec_git('reset', '--hard', 'HEAD') + if @resource.value(:submodules) == :true + exec_git('submodule', 'foreach', '--recursive', 'git', 'clean', '-fd') + exec_git('submodule', 'foreach', '--recursive', 'git', 'reset', '--hard', 'HEAD') + # Ensure that submodules are on the revision specified by the containing repo. + update_submodules + end + end + when :ignore + # nothing to do (rubocop requires code or a comment here) + else + raise Puppet::Error, "Desired repository_status not implemented: #{desired}" + end + end + # Convert working copy to bare # # Moves: diff --git a/lib/puppet/type/vcsrepo.rb b/lib/puppet/type/vcsrepo.rb index 31ed3b62..95b3bcfa 100644 --- a/lib/puppet/type/vcsrepo.rb +++ b/lib/puppet/type/vcsrepo.rb @@ -76,6 +76,9 @@ feature :tmpdir, 'The provider supports setting the temp directory used for wrapper scripts.' + feature :repository_status, + 'The provider supports setting the local repository status (to remove uncommitted local changes).' + ensurable do desc 'Ensure the version control repository.' attr_accessor :latest @@ -355,6 +358,20 @@ def insync?(is) desc 'The temp directory used for wrapper scripts.' end + newproperty :repository_status, required_features: [:repository_status] do + newvalue :default_clean + newvalue :ignore + defaultto :ignore + + def insync?(is) + # unwrap @should + should = @should[0] + return true if should == :ignore + return true if is == should + false + end + end + autorequire(:package) do ['git', 'git-core', 'mercurial', 'subversion'] end diff --git a/spec/unit/puppet/provider/vcsrepo/git_spec.rb b/spec/unit/puppet/provider/vcsrepo/git_spec.rb index dd2b188f..b198294a 100644 --- a/spec/unit/puppet/provider/vcsrepo/git_spec.rb +++ b/spec/unit/puppet/provider/vcsrepo/git_spec.rb @@ -172,6 +172,64 @@ def branch_a_list(include_branch = nil?) end end + context 'when with an ensure of present - with repository_status of ignore' do + it 'does not check the status' do + resource[:repository_status] = :ignore + expect(provider).not_to receive(:exec_git) + provider.repository_status + end + + it 'does not clean' do + expect(provider).not_to receive(:exec_git) + # this calls the setter method + provider.repository_status = :ignore + end + end + + context 'when with an ensure of present - with repository_status of default_clean' do + context 'with defaults' do + it 'checks the status' do + resource[:repository_status] = :default_clean + expect(Dir).to receive(:chdir).with('/tmp/test').at_least(:once).and_yield + expect(provider).to receive(:exec_git).with('status', '--porcelain').and_return('') + provider.repository_status + end + + it 'cleans the repo' do + expect(Dir).to receive(:chdir).with('/tmp/test').at_least(:once).and_yield + expect(provider).to receive(:exec_git).with('clean', '-fd').and_return('') + expect(provider).to receive(:exec_git).with('reset', '--hard', 'HEAD').and_return('') + expect(provider).to receive(:exec_git).with('submodule', 'foreach', '--recursive', 'git', 'clean', '-fd').and_return('') + expect(provider).to receive(:exec_git).with('submodule', 'foreach', '--recursive', 'git', 'reset', '--hard', 'HEAD').and_return('') + expect(provider).to receive(:update_submodules) + # this calls the setter method + provider.repository_status = :default_clean + end + end + + context 'with submodules disabled' do + it 'checks the status' do + resource[:submodules] = :false + resource[:repository_status] = :default_clean + expect(Dir).to receive(:chdir).with('/tmp/test').at_least(:once).and_yield + expect(provider).to receive(:exec_git).with('status', '--porcelain', '--ignore-submodules').and_return('') + provider.repository_status + end + + it 'cleans the repo' do + resource[:submodules] = :false + expect(Dir).to receive(:chdir).with('/tmp/test').at_least(:once).and_yield + expect(provider).to receive(:exec_git).with('clean', '-fd').and_return('') + expect(provider).to receive(:exec_git).with('reset', '--hard', 'HEAD').and_return('') + expect(provider).not_to receive(:update_submodules) + # this calls the setter method + provider.repository_status = :default_clean + end + end + end + end + + context 'when with an ensure of bare' do context 'when with an ensure of bare - with revision' do it 'raises an error' do resource[:ensure] = :bare @@ -202,7 +260,9 @@ def branch_a_list(include_branch = nil?) provider.create end end + end + context 'when with an ensure of mirror' do context 'when with an ensure of mirror - with revision' do it 'raises an error' do resource[:ensure] = :mirror @@ -245,7 +305,7 @@ def branch_a_list(include_branch = nil?) end end - context 'when with an ensure of mirror - when the path is a working copy repository' do + context 'when the path is a working copy repository' do it 'clones overtop it using force' do resource[:force] = true expect(Dir).to receive(:chdir).with('/').once.and_yield @@ -263,7 +323,7 @@ def branch_a_list(include_branch = nil?) end end - context 'when with an ensure of mirror - when the path is not empty and not a repository' do + context 'when the path is not empty and not a repository' do it 'raises an exception' do expect(provider).to receive(:path_exists?).and_return(true) expect(provider).to receive(:path_empty?).and_return(false)