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 >= 1: 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
if 'imported_files' in state:
76
self.imported_files = state['imported_files'];
77
if 'initial_revno' in state:
78
self.initial_revno = state['initial_revno'];
79
if 'affected_files' in state:
80
self.affected_files = state['affected_files'];
61
83
def save_deployment_state( self ):
62
if the.verbose: print "saving deployment state"
84
"""Save the current deployment state (so there will be a deployment ongoing).
87
if the.verbose >= 1: print("saving deployment state")
64
89
# create metadata directory, as necessary
65
90
if not os.path.isdir( the.full_mddir ):
66
91
os.mkdir( the.full_mddir )
93
# pack deployment state
95
'imported_files': self.imported_files,
96
'initial_revno': self.initial_revno,
97
'affected_files': self.affected_files,
69
101
file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
70
102
f = open( file, 'w' )
71
f.write( '\n'.join( self.deploy_files ) + '\n' )
103
f.write( json.dumps( state ) );
74
106
def remove_deployment_state( self ):
107
"""Remove the current deployment state (so no deployment will be ongoing).
76
# delete it, if it exists
77
111
file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
78
112
if( os.path.isfile( file ) ):
115
if the.verbose >= 1: print("removing deployment state")
82
119
def is_ongoing( self ):
83
return False if self.deploy_files is None else True
120
"""Is there a deployment currently ongoing?
123
return False if self.imported_files is None else True
86
126
def check_ongoing( self, ongoing = True ):
127
"""Check that a deployment either is or is not ongoing and raise an error if
88
if self.deploy_files is None:
132
if self.imported_files is None:
89
133
raise self.DeploymentOngoing( False )
91
if self.deploy_files is not None:
135
if self.imported_files is not None:
92
136
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 ) )
139
def get_initial_revno( self ):
140
"""Get the initial revision identifier from the deployment state.
143
return self.initial_revno
146
def copy_in( self, initial_revno = None ):
147
"""Copy-in changes from the home directory to the repository. When finished,
148
the state of deployment is saved, meaning that a deployment is ongoing.
237
151
# check we don't already have a file list
238
152
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 )
154
# if the repo doesn't exist, we have an empty file list
155
if not os.path.exists( the.repo.full_dir ):
156
self.imported_files = list()
159
if the.verbose >= 1: print("importing files...")
160
walker = CopyInWalker()
162
self.imported_files = walker.walk_list
164
# obtain initial revno
165
self.initial_revno = the.repo.vcs.get_revno()
252
168
self.save_deployment_state()
255
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()
348
# check we have a file list
349
self.check_ongoing( True )
351
# we should already have handled conflicts
171
def get_conflicts( self, affected_files = None ):
172
"""Check to see if there are any deployment conflicts. If a list of affected
173
files is supplied, then only those files are checked (and they are also
174
saved with the deployment state). Otherwise, all files in the
175
repository are checked.
178
# check we have a file list
179
self.check_ongoing( True )
182
if affected_files is not None:
183
self.affected_files = affected_files
184
self.save_deployment_state()
186
# check for deployment conflicts
187
walker = ConflictWalker( self.imported_files, self.affected_files )
190
self.conflicts_checked = True
191
return walker.changed + walker.obstructed
194
def copy_out( self, quiet ):
195
"""Copy-out changed files from the repository to the home directory. If the
196
deployment state includes a list of affected files, then only those
197
files are copied-out.
200
# check we have a file list
201
self.check_ongoing( True )
203
# check that deployment conflicts have been checked-for
352
204
if not self.conflicts_checked:
354
'logic error: conflicts should have been checked!' )
205
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 )
208
if the.verbose >= 1: print("exporting files...")
209
walker = CopyOutWalker( self.affected_files, not quiet )
362
213
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
216
class DeploymentOngoing( the.program.FatalError ):
420
218
def __init__( self, ongoing ):