19
19
# along with this program. If not, see <http://www.gnu.org/licenses/>.
22
import os, re, shutil, filecmp
22
import os, re, shutil, filecmp, json
24
from walker.copy_in import CopyInWalker
25
from walker.conflict import ConflictWalker
26
from walker.copy_out import CopyOutWalker
29
32
def __init__( self ):
34
raise RuntimeError( 'logic error: Deployment initialised when '
30
36
self.load_deployment_state()
31
37
self.conflicts_checked = False
34
40
def load_deployment_state( self ):
41
"""Load any deployment state. If one is found then a deployment will be
42
considered to be ongoing.
36
45
# list of files that were copied-in (or at least given the opportunity
37
46
# to be) and updated through the vcs update. This means that, while
38
47
# there may have been conflicts during the update (which will be dealt
39
# with in the repo), any arising conflicts with the filesystem are now
40
# assumed to be because of the vcs update and can be ignored (that is to
41
# say, we no longer care about the file in the home directory). In
42
# short, this is a list of files that it is safe to deploy, regardless
43
# of the state of the filesystem.
48
# with in the repo), any conflicts arising with these files in the home
49
# directory are no longer important and can be ignored. In short, this
50
# is a list of files that it is safe to deploy, regardless of their
51
# state in the home directory.
44
52
self.deploy_files = None
54
# list of files that were affected by a recent vcs update (so only these
55
# files need to be checked for deployment conflicts or copied-out)
56
self.affected_files = None
58
# the revno that the repo was as prior to a recent update
59
self.initial_revno = None
46
61
# do we have a repo?
47
62
if not os.path.exists( the.repo.full_dir ): return
49
64
# if no file list exists, we're done
50
65
file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
51
66
if not os.path.isfile( file ):
52
if the.verbose: print "no deployment state found"
55
69
# read the file list
56
if the.verbose: print "loading deployment state"
70
if the.verbose: print "deployment state found; loading"
57
71
f = open( file, 'r' )
58
self.deploy_files = f.read().splitlines()
72
state = json.loads( f.read() )
74
# unpack deployment state
75
self.deploy_files = state['deploy_files'];
76
self.initial_revno = state['initial_revno'];
77
self.affected_files = state['affected_files'];
61
80
def save_deployment_state( self ):
81
"""Save the current deployment state (so there will be a deployment ongoing).
62
84
if the.verbose: print "saving deployment state"
64
86
# create metadata directory, as necessary
65
87
if not os.path.isdir( the.full_mddir ):
66
88
os.mkdir( the.full_mddir )
90
# pack deployment state
92
'deploy_files': self.deploy_files,
93
'initial_revno': self.initial_revno,
94
'affected_files': self.affected_files,
69
98
file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
70
99
f = open( file, 'w' )
71
f.write( '\n'.join( self.deploy_files ) + '\n' )
100
f.write( json.dumps( state ) );
74
103
def remove_deployment_state( self ):
104
"""Remove the current deployment state (so no deployment will be ongoing).
76
# delete it, if it exists
77
108
file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
78
109
if( os.path.isfile( file ) ):
112
if the.verbose: print "removing deployment state"
82
116
def is_ongoing( self ):
117
"""Is there a deployment currently ongoing?
83
120
return False if self.deploy_files is None else True
86
123
def check_ongoing( self, ongoing = True ):
124
"""Check that a deployment either is or is not ongoing and raise an error if
88
129
if self.deploy_files is None:
89
130
raise self.DeploymentOngoing( False )
92
133
raise self.DeploymentOngoing( True )
95
def confirm( self, message ):
96
# TODO: write interactive confirmation routine
100
def walk_repo( self, dir_func = None, file_func = None, link_func = None,
101
relative_dir = '', ignore_errors = False ):
102
"""Walk through all files and directories in the repo, passing the relative path
103
to each to the provided functions.
105
full_dir = os.path.join( the.repo.full_dir, relative_dir )
106
for file in os.listdir( full_dir ):
107
relative_file = os.path.join( relative_dir, file )
110
if relative_file == '.bzr': continue
112
repo_file = os.path.join( the.repo.full_dir, relative_file )
113
fs_file = os.path.join( the.full_fsdir, relative_file )
114
if os.path.islink( repo_file ):
115
if link_func is not None:
116
link_func( relative_file, repo_file, fs_file )
117
elif os.path.isfile( repo_file ):
118
if file_func is not None:
119
file_func( relative_file, repo_file, fs_file )
120
elif os.path.isdir( repo_file ):
121
if dir_func is not None:
122
dir_func( relative_path, repo_file, fs_file )
123
# recurse in directories
124
self.walk_repo( dir_func, file_func, link_func, relative_dir,
126
elif not ignore_errors:
128
'repo contains unknown/missing entity' )
133
def copy_in_dir( relative_file, dst, src ):
134
self.deploy_files.append( relative_file )
136
# if src doesn't exist, delete it from the repo
137
if not os.path.lexists( src ):
138
if the.verbose: print " _<d " + relative_file
141
# if src is a directory, copy permissions, as necessary
142
elif os.path.isdir( src ):
143
# TODO: should check permissions and only do as necessary
144
if the.verbose: print " d<d " + relative_file
145
shutil.copystat( src, dst )
147
# TODO: serious differences in between ~/ and repo (e.g., files in
148
# one that are directories in the other) should be ignored (e.g.,
149
# not copied-in). And the stuff that is ignored during copy-in
150
# should also be ignored during copy-out and must not be added to
151
# the deploy_files list. Since these ignored files/directories are
152
# transparent to the user, they should have to explicitly permit
153
# them via an ignore file (e.g., ~/.stdhome/.ignore, akin to bzr's
154
# .bzrignore file). If these serious differences are not matched by
155
# the ignore file, an error should show (which will requie a
156
# separate "check" walk of the repo, as is done in copy_out).
158
raise self.Conflict( "%s differs too severely from the repo" %
159
os.path.join( the.fsdir, relative_file ) )
161
def copy_in_file( relative_file, dst, src ):
162
self.deploy_files.append( relative_file )
164
# if src doesn't exist, delete it in the repo
165
if not os.path.lexists( src ):
166
if the.verbose: print " ?<f " + relative_file
169
# if src is a symlink, replace it in repo
170
elif os.path.islink( src ):
171
if the.verbose: print " l<f " + relative_file
173
os.symlink( os.readlink( src ), dst )
175
# if src is a file, replace it if different
176
elif os.path.isfile( src ):
177
if not filecmp.cmp( src, dst ):
178
if the.verbose: print " f<f " + relative_file
180
shutil.copy( src, dst )
181
shutil.copystat( src, dst )
183
if the.verbose: print " f=f " + relative_file
185
# TODO: serious differences in between ~/ and repo (e.g., files in
186
# one that are directories in the other) should be ignored (e.g.,
187
# not copied-in). And the stuff that is ignored during copy-in
188
# should also be ignored during copy-out and must not be added to
189
# the deploy_files list. Since these ignored files/directories are
190
# transparent to the user, they should have to explicitly permit
191
# them via an ignore file (e.g., ~/.stdhome/.ignore, akin to bzr's
192
# .bzrignore file). If these serious differences are not matched by
193
# the ignore file, an error should show (which will requie a
194
# separate "check" walk of the repo, as is done in copy_out).
196
raise self.Conflict( "%s differs too severely from the repo" %
197
os.path.join( the.fsdir, relative_file ) )
199
def copy_in_link( relative_file, dst, src ):
200
self.deploy_files.append( relative_file )
202
# if src doesn't exist, delete it in the repo
203
if not os.path.lexists( src ):
204
if the.verbose: print " _<l " + relative_file
207
# if src is a symlink, replace it if different in repo
208
elif os.path.islink( src ):
209
if os.readlink( src ) != os.readlink( dst ):
210
if the.verbose: print " l<l " + relative_file
212
os.symlink( os.readlink( src ), dst )
214
if the.verbose: print " l=l " + relative_file
216
# if src is a file, replace it in repo
217
elif os.path.isfile( src ):
218
if the.verbose: print " f<l " + relative_file
220
shutil.copy( src, dst )
221
shutil.copystat( src, dst )
223
# TODO: serious differences in between ~/ and repo (e.g., files in
224
# one that are directories in the other) should be ignored (e.g.,
225
# not copied-in). And the stuff that is ignored during copy-in
226
# should also be ignored during copy-out and must not be added to
227
# the deploy_files list. Since these ignored files/directories are
228
# transparent to the user, they should have to explicitly permit
229
# them via an ignore file (e.g., ~/.stdhome/.ignore, akin to bzr's
230
# .bzrignore file). If these serious differences are not matched by
231
# the ignore file, an error should show (which will requie a
232
# separate "check" walk of the repo, as is done in copy_out).
234
raise self.Conflict( "%s differs too severely from the repo" %
235
os.path.join( the.fsdir, relative_file ) )
136
def get_initial_revno( self ):
137
"""Get the initial revision identifier from the deployment state.
140
return self.initial_revno
143
def copy_in( self, initial_revno = None ):
144
"""Copy-in changes from the home directory to the repository. When finished,
145
the state of deployment is saved, meaning that a deployment is ongoing.
237
148
# check we don't already have a file list
238
149
self.check_ongoing( False )
241
self.deploy_files = list()
243
# if the repo doesn't exist, we're done
244
if not os.path.exists( the.repo.full_dir ): return
247
if the.verbose: print "importing files"
248
self.walk_repo( dir_func = copy_in_dir, file_func = copy_in_file,
249
link_func = copy_in_link, ignore_errors = True )
151
# if the repo doesn't exist, we have an empty file list
152
if not os.path.exists( the.repo.full_dir ):
153
self.deploy_files = list()
156
if the.verbose: print "importing files..."
157
walker = CopyInWalker()
159
if( walker.changed ):
160
raise self.CopyInConflicts( walker.changed )
161
self.deploy_files = walker.walk_list
163
# obtain initial revno
164
self.initial_revno = the.repo.vcs.get_revno()
252
167
self.save_deployment_state()
170
def get_conflicts( self, affected_files = None ):
171
"""Check to see if there are any delpoyment conflicts. If a list of affected
172
files is supplied, then only those files are checked (and they are also
173
saved with the deployment state). Otherwise, all files in the
174
repository are checked.
177
# check we have a file list
178
self.check_ongoing( True )
181
if affected_files is not None:
182
self.affected_files = affected_files
183
self.save_deployment_state()
185
# check for deployment conflicts
186
walker = ConflictWalker( self.deploy_files, self.affected_files )
189
self.conflicts_checked = True
190
return walker.changed + walker.obstructed
255
193
def copy_out( self ):
257
def copy_out_dir( relative_file, src, dst ):
259
# if dst doesn't exist, create it
260
if not os.path.lexists( dst ):
261
if the.verbose: print " d>_ " + relative_file
263
shutil.copystat( src, dst )
265
# if dst is a file/symlink, replace it
266
elif os.path.isfile( dst ):
267
if the.verbose: print " d>f " + relative_file
270
shutil.copystat( src, dst )
272
# if dst is a directory, copy permissions as required
273
elif os.path.isdir( dst ):
274
# TODO: should check permission and only do as necessary
275
if the.verbose: print " d>d " + relative_file
276
shutil.copystat( src, dst )
279
raise NotImplementedError()
281
def copy_out_file( relative_file, src, dst ):
283
# if dst doesn't exist, copy
284
if not os.path.lexists( dst ):
285
if the.verbose: print " f>_ " + relative_file
287
shutil.copy( src, dst )
288
shutil.copystat( src, dst )
290
# if dst is a symlink, replace it
291
elif os.path.islink( dst ):
292
if the.verbose: print " f>l " + relative_file
294
shutil.copy( src, dst )
295
shutil.copystat( src, dst )
297
# if dst is a file, replace it if different
298
elif os.path.isfile( dst ):
299
if not filecmp.cmp( src, dst ):
300
if the.verbose: print " f>f " + relative_file
302
shutil.copy( src, dst )
303
shutil.copystat( src, dst )
305
if the.verbose: print " f=f " + relative_file
307
# if dst is a directory, replace it
308
elif os.path.isdir( dst ):
309
if the.verbose: print " f>d " + relative_file
311
shutil.copy( src, dst )
312
shutil.copystat( src, dst )
315
raise NotImplementedError()
317
def copy_out_link( relative_file, src, dst ):
319
# if dst doesn't exist, copy
320
if not os.path.lexists( dst ):
321
if the.verbose: print " l>_ " + relative_file
322
os.symlink( os.readlink( src ), dst )
324
# if dst is a symlink, replace it if different
325
elif os.path.islink( dst ):
326
if os.readlink( src ) != os.readlink( dst ):
327
if the.verbose: print " l>l " + relative_file
329
os.symlink( os.readlink( src ), dst )
331
if the.verbose: print " l=l " + relative_file
333
# if dst is a file, replace it
334
elif os.path.isfile( dst ):
335
if the.verbose: print " l>f " + relative_file
337
os.symlink( os.readlink( src ), dst )
339
# if dst is a directory, replace it
340
elif os.path.isdir( dst ):
341
if the.verbose: print " l>d " + relative_file
343
os.symlink( os.readlink( src ), dst )
346
raise NotImplementedError()
194
"""Copy-out changed files frmo the repository to the home directory. If the
195
deployment state incudes a list of affected files, then only those fiels
348
199
# check we have a file list
349
200
self.check_ongoing( True )
351
# we should already have handled conflicts
202
# check that deployment conflicts have been checked-for
352
203
if not self.conflicts_checked:
354
'logic error: conflicts should have been checked!' )
204
raise RuntimeError('logic error: deployment conflicts unchecked' )
357
if the.verbose: print "exporting files"
358
self.walk_repo( dir_func = copy_out_dir, file_func = copy_out_file,
359
link_func = copy_out_link, ignore_errors = True )
207
if the.verbose: print "exporting files..."
208
walker = CopyOutWalker( self.affected_files )
362
212
self.remove_deployment_state()
365
def get_conflicts( self ):
367
def check_dir( relative_file, src, dst ):
369
# files that existed at copy-in can be copied-out and overwritten
370
if self.deploy_files is not None and \
371
relative_file in self.deploy_files: return
373
# files that don't exist in the filesystem can be copied-out
374
if not os.path.lexists( dst ): return
376
# accept/reject existing files
377
if os.path.islink( dst ):
378
self.files.append( "%s already exists (as a symlink)" %
379
os.path.join( the.fsdir, relative_file ) )
380
elif os.path.isfile( dst ):
381
self.files.append( "%s already exists (as a file)" %
382
os.path.join( the.fsdir, relative_file ) )
383
elif os.path.isdir( dst ):
386
self.files.append( "%s already exists" %
387
os.path.join( the.fsdir, relative_file ) )
389
def check_file( relative_file, src, dst ):
391
# files that existed at copy-in can be copied-out and overwritten
392
if self.deploy_files is not None and \
393
relative_file in self.deploy_files: return
395
# files that don't exist in the filesystem can be copied-out
396
if not os.path.lexists( dst ): return
398
# accept/reject existing files
399
if os.path.isfile( dst ):
400
self.files.append( "%s already exists" %
401
os.path.join( the.fsdir, relative_file ) )
402
elif os.path.isdir( dst ):
403
self.files.append( "%s already exists (as a directory)" %
404
os.path.join( the.fsdir, relative_file ) )
406
self.files.append( "%s already exists" %
407
os.path.join( the.fsdir, relative_file ) )
409
self.conflicts_checked = True
411
# check for conflicts
413
self.walk_repo( dir_func = check_dir, file_func = check_file,
414
link_func = check_file )
418
215
class DeploymentOngoing( the.program.FatalError ):
420
217
def __init__( self, ongoing ):