Remove excess whitespace
[puppet_vcsrepo.git] / lib / puppet / provider / vcsrepo / p4.rb
1 require File.join(File.dirname(__FILE__), '..', 'vcsrepo')
2
3 Puppet::Type.type(:vcsrepo).provide(:p4, :parent => Puppet::Provider::Vcsrepo) do
4   desc "Supports Perforce depots"
5
6   has_features :filesystem_types, :reference_tracking, :p4config
7
8   def create
9     # create or update client
10     create_client(client_name)
11
12     # if source provided, sync client
13     source = @resource.value(:source)
14     if source
15       revision = @resource.value(:revision)
16       sync_client(source, revision)
17     end
18
19     update_owner
20   end
21
22   def working_copy_exists?
23     # Check if the server is there, or raise error
24     p4(['info'], {:marshal => false})
25
26     # Check if workspace is setup
27     args = ['where']
28     args.push(@resource.value(:path) + "...")
29     hash = p4(args, {:raise => false})
30
31     return (hash['code'] != "error")
32   end
33
34   def exists?
35     working_copy_exists?
36   end
37
38   def destroy
39     args = ['client']
40     args.push('-d', '-f')
41     args.push(client_name)
42     p4(args)
43     FileUtils.rm_rf(@resource.value(:path))
44   end
45
46   def latest?
47     rev = self.revision
48     if rev
49       (rev >= self.latest)
50     else
51       true
52     end
53   end
54
55   def latest
56     args = ['changes']
57     args.push('-m1', @resource.value(:source))
58     hash = p4(args)
59
60     return hash['change'].to_i
61   end
62
63   def revision
64     args = ['cstat']
65     args.push(@resource.value(:source))
66     hash = p4(args, {:marshal => false})
67     hash = marshal_cstat(hash)
68
69     revision = 0
70     if hash && hash['code'] != 'error'
71       hash['data'].each do |c|
72         if c['status'] == 'have'
73           change = c['change'].to_i
74           revision = change if change > revision
75         end
76       end
77     end
78     return revision
79   end
80
81   def revision=(desired)
82     sync_client(@resource.value(:source), desired)
83     update_owner
84   end
85
86   private
87
88   def update_owner
89     if @resource.value(:owner) or @resource.value(:group)
90       set_ownership
91     end
92   end
93
94   # Sync the client workspace files to head or specified revision.
95   # Params:
96   # +source+:: Depot path to sync
97   # +revision+:: Perforce change list to sync to (optional)
98   def sync_client(source, revision)
99     Puppet.debug "Syncing: #{source}"
100     args = ['sync']
101     if revision
102       args.push(source + "@#{revision}")
103     else
104       args.push(source)
105     end
106     p4(args)
107   end
108
109   # Returns the name of the Perforce client workspace
110   def client_name
111     p4config = @resource.value(:p4config)
112
113     # default (generated) client name
114     path = @resource.value(:path)
115     host = Facter.value('hostname')
116     default = "puppet-" + Digest::MD5.hexdigest(path + host)
117
118     # check config for client name
119     set_client = nil
120     if p4config && File.file?(p4config)
121       open(p4config) do |f|
122         m = f.grep(/^P4CLIENT=/).pop
123         p = /^P4CLIENT=(.*)$/
124         set_client = p.match(m)[1] if m
125       end
126     end
127
128     return set_client || ENV['P4CLIENT'] || default
129   end
130
131   # Create (or update) a client workspace spec.
132   # If a client name is not provided then a hash based on the path is used.
133   # Params:
134   # +client+:: Name of client workspace
135   # +path+:: The Root location of the Perforce client workspace
136   def create_client(client)
137     Puppet.debug "Creating client: #{client}"
138
139     # fetch client spec
140     hash = parse_client(client)
141     hash['Root'] = @resource.value(:path)
142     hash['Description'] = "Generated by Puppet VCSrepo"
143
144     # check is source is a Stream
145     source = @resource.value(:source)
146     if source
147       parts = source.split(/\//)
148       if parts && parts.length >= 4
149         source = "//" + parts[2] + "/" + parts[3]
150         streams = p4(['streams', source], {:raise => false})
151         if streams['code'] == "stat"
152           hash['Stream'] = streams['Stream']
153           notice "Streams" + streams['Stream'].inspect
154         end
155       end
156     end
157
158     # save client spec
159     save_client(hash)
160   end
161
162
163   # Fetches a client workspace spec from Perforce and returns a hash map representation.
164   # Params:
165   # +client+:: name of the client workspace
166   def parse_client(client)
167     args = ['client']
168     args.push('-o', client)
169     hash = p4(args)
170
171     return hash
172   end
173
174
175   # Saves the client workspace spec from the given hash
176   # Params:
177   # +hash+:: hash map of client spec
178   def save_client(hash)
179     spec = String.new
180     view = "\nView:\n"
181
182     hash.keys.sort.each do |k|
183       v = hash[k]
184       next if( k == "code" )
185       if(k.to_s =~ /View/ )
186         view += "\t#{v}\n"
187       else
188         spec += "#{k.to_s}: #{v.to_s}\n"
189       end
190     end
191     spec += view
192
193     args = ['client']
194     args.push('-i')
195     p4(args, {:input => spec, :marshal => false})
196   end
197
198   # Sets Perforce Configuration environment.
199   # P4CLIENT generated, but overwitten if defined in config.
200   def config
201     p4config = @resource.value(:p4config)
202
203     cfg = Hash.new
204     cfg.store 'P4CONFIG', p4config if p4config
205     cfg.store 'P4CLIENT', client_name
206     return cfg
207   end
208
209   def p4(args, options = {})
210     # Merge custom options with defaults
211     opts = {
212       :raise    => true,    # Raise errors
213       :marshal  => true,    # Marshal output
214     }.merge(options)
215
216     cmd = ['p4']
217     cmd.push '-R' if opts[:marshal]
218     cmd.push args
219     cmd_str = cmd.respond_to?(:join) ? cmd.join(' ') : cmd
220
221     Puppet.debug "environment: #{config}"
222     Puppet.debug "command: #{cmd_str}"
223
224     hash = Hash.new
225     Open3.popen3(config, cmd_str) do |i, o, e, t|
226       # Send input stream if provided
227       if(opts[:input])
228         Puppet.debug "input:\n" + opts[:input]
229         i.write opts[:input]
230         i.close
231       end
232
233       if(opts[:marshal])
234         hash = Marshal.load(o)
235       else
236         hash['data'] = o.read
237       end
238
239       # Raise errors, Perforce or Exec
240       if(opts[:raise] && !e.eof && t.value != 0)
241         raise Puppet::Error, "\nP4: #{e.read}"
242       end
243       if(opts[:raise] && hash['code'] == 'error' && t.value != 0)
244         raise Puppet::Error, "\nP4: #{hash['data']}"
245       end
246     end
247
248     Puppet.debug "hash: #{hash}\n"
249     return hash
250   end
251
252   # helper method as cstat does not Marshal
253   def marshal_cstat(hash)
254     data = hash['data']
255     code = 'error'
256
257     list = Array.new
258     change = Hash.new
259     data.each_line do |l|
260       p = /^\.\.\. (.*) (.*)$/
261       m = p.match(l)
262       if m
263         change[m[1]] = m[2]
264         if m[1] == 'status'
265           code = 'stat'
266           list.push change
267           change = Hash.new
268         end
269       end
270     end
271
272     hash = Hash.new
273     hash.store 'code', code
274     hash.store 'data', list
275     return hash
276   end
277
278 end