Style adjustment
[puppet_vcsrepo.git] / lib / puppet / provider / vcsrepo / git.rb
1 require File.join(File.dirname(__FILE__), '..', 'vcsrepo')
2
3 Puppet::Type.type(:vcsrepo).provide(:git, :parent => Puppet::Provider::Vcsrepo) do
4   desc "Supports Git repositories"
5
6   ##TODO modify the commands below so that the su - is included
7   optional_commands :git => 'git',
8                     :su  => 'su'
9   has_features :bare_repositories, :reference_tracking, :ssh_identity, :multiple_remotes, :user
10
11   def create
12     if !@resource.value(:source)
13       init_repository(@resource.value(:path))
14     else
15       clone_repository(@resource.value(:source), @resource.value(:path))
16       if @resource.value(:revision)
17         if @resource.value(:ensure) == :bare
18           notice "Ignoring revision for bare repository"
19         else
20           checkout
21         end
22       end
23       if @resource.value(:ensure) != :bare
24         update_submodules
25       end
26     end
27     update_owner_and_excludes
28   end
29
30   def destroy
31     FileUtils.rm_rf(@resource.value(:path))
32   end
33
34   def latest?
35     at_path do
36       return self.revision == self.latest
37     end
38   end
39
40   def latest
41     branch = on_branch?
42     if branch == 'master'
43       return get_revision("#{@resource.value(:remote)}/HEAD")
44     elsif branch == '(no branch)'
45       return get_revision('HEAD')
46     else
47       return get_revision("#{@resource.value(:remote)}/%s" % branch)
48     end
49   end
50
51   def revision
52     update_references
53     current = at_path { git_with_identity('rev-parse', 'HEAD').chomp }
54     return current unless @resource.value(:revision)
55
56     if tag_revision?(@resource.value(:revision))
57       canonical = at_path { git_with_identity('show', @resource.value(:revision)).scan(/^commit (.*)/).to_s }
58     else
59       # if it's not a tag, look for it as a local ref
60       canonical = at_path { git_with_identity('rev-parse', '--revs-only', @resource.value(:revision)).chomp }
61       if canonical.empty?
62         # git rev-parse executed properly but didn't find the ref;
63         # look for it in the remote
64         remote_ref = at_path { git_with_identity('ls-remote', '--heads', '--tags', @resource.value(:remote), @resource.value(:revision)).chomp }
65         if remote_ref.empty?
66           fail("#{@resource.value(:revision)} is not a local or remote ref")
67         end
68
69         # $ git ls-remote --heads --tags origin feature/cvs
70         # 7d4244b35e72904e30130cad6d2258f901c16f1a      refs/heads/feature/cvs
71         canonical = remote_ref.split.first
72       end
73     end
74
75     if current == canonical
76       @resource.value(:revision)
77     else
78       current
79     end
80   end
81
82   def revision=(desired)
83     checkout(desired)
84     if local_branch_revision?(desired)
85       # reset instead of pull to avoid merge conflicts. assuming remote is
86       # authoritative.
87       # might be worthwhile to have an allow_local_changes param to decide
88       # whether to reset or pull when we're ensuring latest.
89       at_path { git_with_identity('reset', '--hard', "#{@resource.value(:remote)}/#{desired}") }
90     end
91     if @resource.value(:ensure) != :bare
92       update_submodules
93     end
94     update_owner_and_excludes
95   end
96
97   def bare_exists?
98     bare_git_config_exists? && !working_copy_exists?
99   end
100
101   def working_copy_exists?
102     File.directory?(File.join(@resource.value(:path), '.git'))
103   end
104
105   def exists?
106     working_copy_exists? || bare_exists?
107   end
108
109   def update_remote_origin_url
110     current = git_with_identity('config', 'remote.origin.url')
111     unless @resource.value(:source).nil?
112       if current.nil? or current.strip != @resource.value(:source)
113         git_with_identity('config', 'remote.origin.url', @resource.value(:source))
114       end
115     end
116   end
117
118   def update_references
119     at_path do
120       update_remote_origin_url
121       git_with_identity('fetch', @resource.value(:remote))
122       git_with_identity('fetch', '--tags', @resource.value(:remote))
123       update_owner_and_excludes
124     end
125   end
126
127   private
128
129   def bare_git_config_exists?
130     File.exist?(File.join(@resource.value(:path), 'config'))
131   end
132
133   def clone_repository(source, path)
134     check_force
135     args = ['clone']
136     if @resource.value(:ensure) == :bare
137       args << '--bare'
138     end
139     if !File.exist?(File.join(@resource.value(:path), '.git'))
140       args.push(source, path)
141       Dir.chdir("/") do
142         git_with_identity(*args)
143       end
144     else
145       notice "Repo has already been cloned"
146     end
147   end
148
149   def check_force
150     if path_exists?
151       if @resource.value(:force)
152         notice "Removing %s to replace with vcsrepo." % @resource.value(:path)
153         destroy
154       else
155         raise Puppet::Error, "Could not create repository (non-repository at path)"
156       end
157     end
158   end
159
160   def init_repository(path)
161     check_force
162     if @resource.value(:ensure) == :bare && working_copy_exists?
163       convert_working_copy_to_bare
164     elsif @resource.value(:ensure) == :present && bare_exists?
165       convert_bare_to_working_copy
166     else
167       # normal init
168       FileUtils.mkdir(@resource.value(:path))
169       FileUtils.chown(@resource.value(:user), nil, @resource.value(:path)) if @resource.value(:user)
170       args = ['init']
171       if @resource.value(:ensure) == :bare
172         args << '--bare'
173       end
174       at_path do
175         git_with_identity(*args)
176       end
177     end
178   end
179
180   # Convert working copy to bare
181   #
182   # Moves:
183   #   <path>/.git
184   # to:
185   #   <path>/
186   def convert_working_copy_to_bare
187     notice "Converting working copy repository to bare repository"
188     FileUtils.mv(File.join(@resource.value(:path), '.git'), tempdir)
189     FileUtils.rm_rf(@resource.value(:path))
190     FileUtils.mv(tempdir, @resource.value(:path))
191   end
192
193   # Convert bare to working copy
194   #
195   # Moves:
196   #   <path>/
197   # to:
198   #   <path>/.git
199   def convert_bare_to_working_copy
200     notice "Converting bare repository to working copy repository"
201     FileUtils.mv(@resource.value(:path), tempdir)
202     FileUtils.mkdir(@resource.value(:path))
203     FileUtils.mv(tempdir, File.join(@resource.value(:path), '.git'))
204     if commits_in?(File.join(@resource.value(:path), '.git'))
205       reset('HEAD')
206       git_with_identity('checkout', '--force')
207       update_owner_and_excludes
208     end
209   end
210
211   def commits_in?(dot_git)
212     Dir.glob(File.join(dot_git, 'objects/info/*'), File::FNM_DOTMATCH) do |e|
213       return true unless %w(. ..).include?(File::basename(e))
214     end
215     false
216   end
217
218   def checkout(revision = @resource.value(:revision))
219     if !local_branch_revision? && remote_branch_revision?
220       at_path { git_with_identity('checkout', '-b', revision, '--track', "#{@resource.value(:remote)}/#{revision}") }
221     else
222       at_path { git_with_identity('checkout', '--force', revision) }
223     end
224   end
225
226   def reset(desired)
227     at_path do
228       git_with_identity('reset', '--hard', desired)
229     end
230   end
231
232   def update_submodules
233     at_path do
234       git_with_identity('submodule', 'update', '--init', '--recursive')
235     end
236   end
237
238   def remote_branch_revision?(revision = @resource.value(:revision))
239     # git < 1.6 returns '#{@resource.value(:remote)}/#{revision}'
240     # git 1.6+ returns 'remotes/#{@resource.value(:remote)}/#{revision}'
241     branch = at_path { branches.grep /(remotes\/)?#{@resource.value(:remote)}\/#{revision}/ }
242     branch unless branch.empty?
243   end
244
245   def local_branch_revision?(revision = @resource.value(:revision))
246     at_path { branches.include?(revision) }
247   end
248
249   def tag_revision?(revision = @resource.value(:revision))
250     at_path { tags.include?(revision) }
251   end
252
253   def branches
254     at_path { git_with_identity('branch', '-a') }.gsub('*', ' ').split(/\n/).map { |line| line.strip }
255   end
256
257   def on_branch?
258     at_path { git_with_identity('branch', '-a') }.split(/\n/).grep(/\*/).first.to_s.gsub('*', '').strip
259   end
260
261   def tags
262     at_path { git_with_identity('tag', '-l') }.split(/\n/).map { |line| line.strip }
263   end
264
265   def set_excludes
266     at_path { open('.git/info/exclude', 'w') { |f| @resource.value(:excludes).each { |ex| f.write(ex + "\n") }}}
267   end
268
269   def get_revision(rev)
270     if !working_copy_exists?
271       create
272     end
273     at_path do
274       update_remote_origin_url
275       git_with_identity('fetch', @resource.value(:remote))
276       git_with_identity('fetch', '--tags', @resource.value(:remote))
277     end
278     current = at_path { git_with_identity('rev-parse', rev).strip }
279     if @resource.value(:revision)
280       if local_branch_revision?
281         canonical = at_path { git_with_identity('rev-parse', @resource.value(:revision)).strip }
282       elsif remote_branch_revision?
283         canonical = at_path { git_with_identity('rev-parse', "#{@resource.value(:remote)}/" + @resource.value(:revision)).strip }
284       end
285       current = @resource.value(:revision) if current == canonical
286     end
287     update_owner_and_excludes
288     return current
289   end
290
291   def update_owner_and_excludes
292     if @resource.value(:owner) or @resource.value(:group)
293       set_ownership
294     end
295     if @resource.value(:excludes)
296       set_excludes
297     end
298   end
299
300   def git_with_identity(*args)
301     if @resource.value(:identity)
302       Tempfile.open('git-helper') do |f|
303         f.puts '#!/bin/sh'
304         f.puts "exec ssh -oStrictHostKeyChecking=no -oPasswordAuthentication=no -oKbdInteractiveAuthentication=no -oChallengeResponseAuthentication=no -oConnectTimeout=120 -i #{@resource.value(:identity)} $*"
305         f.close
306
307         FileUtils.chmod(0755, f.path)
308         env_save = ENV['GIT_SSH']
309         ENV['GIT_SSH'] = f.path
310
311         ret = git(*args)
312
313         ENV['GIT_SSH'] = env_save
314
315         return ret
316       end
317     elsif @resource.value(:user)
318       su(@resource.value(:user), '-c', "git #{args.join(' ')}" )
319     else
320       git(*args)
321     end
322   end
323 end