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

support force-cleaning git repos #632

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -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`

Expand Down Expand Up @@ -837,20 +860,21 @@ 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`.)
* `gzip_compression` - Supports explicit GZip compression levels. (Available with `cvs`.)
* `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`.)

<a id="limitations"></a>
## Limitations
Expand Down
50 changes: 49 additions & 1 deletion lib/puppet/provider/vcsrepo/git.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
17 changes: 17 additions & 0 deletions lib/puppet/type/vcsrepo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
64 changes: 62 additions & 2 deletions spec/unit/puppet/provider/vcsrepo/git_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
Loading