/stdhome

To get this branch, use:
bzr branch http://bzr.ed.am/stdhome

« back to all changes in this revision

Viewing changes to lib/stdhome/vcs/bzr.py

  • Committer: Tim Marston
  • Date: 2014-02-12 21:51:08 UTC
  • Revision ID: tim@ed.am-20140212215108-stk5z0nlvgpi4oa8
added bzr as a vcs backend; finished init command; implemented deployment

Show diffs side-by-side

added added

removed removed

21
21
 
22
22
import subprocess, os, re, shutil
23
23
from subprocess import Popen
 
24
import stdhome.the as the
24
25
import StringIO
25
 
from vcs import Vcs
26
 
from stdhome import the
27
 
 
28
 
 
29
 
class BzrVcs( Vcs ):
 
26
 
 
27
 
 
28
class VcsBzr:
30
29
 
31
30
 
32
31
        def __init__( self, dir ):
34
33
 
35
34
                @param dir the fully-qualified directory to work in.
36
35
                """
37
 
 
38
36
                self.dir = dir
39
 
                self.ignored_files = [ '.bzr', '.bzrignore' ]
40
 
 
41
 
 
42
 
        def has_authority( self ):
43
 
                """Check that the directory is under this VCS's control.
44
 
                """
45
 
 
46
 
                return os.path.exists( os.path.join( self.dir, '.bzr' ) )
47
 
 
48
 
 
49
 
        def expand_repo_url( self, url ):
50
 
                """Convert a simple hostname in to an URL that the VCS can use.
51
 
                """
52
 
 
53
 
                return 'bzr+ssh://%s/%s/%s' % ( url, the.dir, the.repo.name )
54
37
 
55
38
 
56
39
        def init( self ):
61
44
                os.mkdir( self.dir )
62
45
 
63
46
                # bzr init
64
 
                try:
65
 
                        self.run( [ 'bzr', 'init', '.' ] )
66
 
                except self.VcsError as e:
 
47
                p = Popen( [ 'bzr', 'init', '.' ], cwd = self.dir,
 
48
                                   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
 
49
                output = p.communicate()[ 0 ]
 
50
                if p.returncode > 0:
67
51
 
68
52
                        # attempt to clean-up dir
69
53
                        try:
71
55
                        except OSError:
72
56
                                pass
73
57
 
74
 
                        raise
 
58
                        raise the.program.FatalError( 'bzr init failed', output )
75
59
 
76
60
 
77
61
        def checkout( self, url ):
84
68
                os.mkdir( self.dir )
85
69
 
86
70
                # bzr co
87
 
                try:
88
 
                        self.run( [ 'bzr', 'checkout', url, '.' ] )
89
 
                except self.VcsError as e:
 
71
                p = Popen( [ 'bzr', 'co', url, '.' ], cwd = self.dir,
 
72
                                   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
 
73
                output = p.communicate()[ 0 ]
 
74
                if p.returncode > 0:
90
75
 
91
76
                        # attempt to clean-up dir
92
77
                        try:
94
79
                        except OSError:
95
80
                                pass
96
81
 
97
 
                        raise
98
 
 
99
 
 
100
 
        def get_revno( self ):
101
 
                """Obtain some sort of revision identifier
102
 
                """
103
 
 
104
 
                # bzr revno
105
 
                output = self.run( [ 'bzr', 'revno', '--tree' ] )
106
 
 
107
 
                # parse revno
108
 
                buf = StringIO.StringIO( output )
109
 
                return buf.readline().rstrip()
110
 
 
111
 
 
112
 
        def revert( self, revno = None ):
 
82
                        raise the.program.FatalError( 'bzr checkout failed', output )
 
83
 
 
84
 
 
85
        def revert( self ):
113
86
                """Revert the branch so that there are no outstanding changes or unknown files.
114
 
                If a revno is supplied, then the repository is reverted to that
115
 
                revision.
116
87
                """
117
88
 
118
 
                # bzr st
119
 
                output = self.run( [ 'bzr', 'status' ] )
120
 
                files = self.parse_file_blocks( output )
121
 
 
122
 
                # remove kind changed files (or they can cause `bzr revert` to break in
123
 
                # strange situations, like when a directory has been replaced with a
124
 
                # symlink to a non-existant file)
125
 
                if 'kind changed' in files:
126
 
                        for file in files[ 'kind changed' ]:
127
 
                                matches = re.search( '^(.+?)[/@+]? \([^)]+\)$', file )
128
 
                                if not matches:
129
 
                                        raise RunTimeError(
130
 
                                                'failed to parse bzr kind change: %s' % file )
131
 
                                file = matches.group( 1 )
132
 
                                if the.verbose >= 2: print "removing (kind changed): " + file
133
 
                                full_file = os.path.join( self.dir, file )
134
 
                                if os.path.isfile( full_file ) or os.path.islink( full_file ):
135
 
                                        os.unlink( full_file )
136
 
                                elif os.path.isdir( full_file ):
137
 
                                        shutil.rmtree( full_file )
138
 
                                else:
139
 
                                        raise RuntimeError( 'exotic file in repo: %s' % file )
140
 
 
141
89
                # bzr revert
142
 
                self.run( [ 'bzr', 'revert', '--no-backup' ] )
 
90
                p = Popen( [ 'bzr', 'revert', '--no-backup' ], cwd = self.dir,
 
91
                                   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
 
92
                output = p.communicate()[ 0 ]
 
93
                if p.returncode > 0:
 
94
                        raise the.program.FatalError( 'bzr revert failed', output )
143
95
 
144
96
                # bzr st
145
 
                output = self.run( [ 'bzr', 'status' ] )
 
97
                p = Popen( [ 'bzr', 'st' ], cwd = self.dir,
 
98
                                   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
 
99
                output = p.communicate()[ 0 ]
 
100
                if p.returncode > 0:
 
101
                        raise the.program.FatalError( 'bzr status failed', output )
146
102
                files = self.parse_file_blocks( output )
147
103
 
148
104
                # remove unknown files
149
105
                if 'unknown' in files:
150
106
                        for file in files[ 'unknown' ]:
151
 
                                matches = re.search( r'^(.+?)[/@+]?$', file )
152
 
                                if not matches:
153
 
                                        raise RunTimeError(
154
 
                                                'failed to parse bzr unknowns: %s' % file )
155
 
                                file = matches.group( 1 )
156
 
                                if the.verbose >= 2: print "removing (unknown): " + file
157
107
                                full_file = os.path.join( self.dir, file )
158
 
                                if os.path.isfile( full_file ) or os.path.islink( full_file ):
 
108
                                if os.path.isfile( full_file ):
159
109
                                        os.unlink( full_file )
160
 
                                elif os.path.isdir( full_file ):
 
110
                                elif os.full_file.isdir( full_file ):
161
111
                                        shutil.rmtree( full_file )
162
112
                                else:
163
113
                                        raise RuntimeError( 'exotic file in repo: %s' % file )
164
114
 
165
 
                # if a revision identifier has been given, ensure we're updated to that
166
 
                if revno is not None and self.get_revno() != revno:
167
 
 
168
 
                        # bzr update
169
 
                        self.run( [ 'bzr', 'update', '-r', revno ] )
170
 
 
171
115
 
172
116
        def update( self ):
173
 
                """Update the branch, pulling down any upstream changes and merging them.  This
174
 
                method returns a list of the files that were modified as part of this
175
 
                operation.
 
117
                """Update the branch, pulling down any upstream changes and merging them.
176
118
                """
177
119
 
178
 
#               WARNING: the following might cause bzr to ask for your ssh password more than
179
 
#               once during an update!!!
180
 
#
181
 
#               # get revno
182
 
#               revno = self.get_revno()
183
 
#
184
 
#               # update to current revision (pull in history without updating tree)
185
 
#               self.run( [ 'bzr', 'update', '-r', revno ] )
186
 
#
187
 
#               # get log output
188
 
#               next_revno = str( int( revno ) + 1 )
189
 
#               output = self.run( [ 'bzr', 'log', '-r', next_revno + '..' ] )
190
 
#
191
 
#               # parse output
192
 
#               keep_files = list()
193
 
#               buf = StringIO.StringIO( output )
194
 
#               in_message = False
195
 
#               for line in buf:
196
 
#                       line = line.rstrip( '\n' )
197
 
#                       if line.lower() == 'message:':
198
 
#                               in_message = True
199
 
#                       elif in_message:
200
 
#                               if line[ : 2 ] != '  ':
201
 
#                                       in_message = False
202
 
#                               else:
203
 
#                                       line = line[ 2 : ]
204
 
#
205
 
#                                       # process directives
206
 
#                                       if line[ : 6 ].lower() == 'keep: ':
207
 
#                                               file = line[ 6 : ]
208
 
#                                               if file in rename_files: file = rename_files[ file ]
209
 
#                                               keep_files.append( file )
210
 
#                                       elif line[ : 8 ].lower() == 'rename: ':
211
 
#                                               rename_from = line[ 8 : ]
212
 
#                                       elif line[ : 4 ].lower() == 'to: ':
213
 
#                                               if rename_from in rename_files:
214
 
#                                                       rename_from = rename_files[ rename_from ]
215
 
#                                               rename_files[ line[ 4 : ] ] = rename_from
216
 
 
217
 
                # bzr update properly
218
 
                output = self.run( [ 'bzr', 'update' ] )
219
 
 
220
 
                # parse output (see logic in report() in bzrlib/delta.py)
221
 
                files = list()
222
 
                buf = StringIO.StringIO( output )
223
 
                for line in buf:
224
 
                        if not re.search( '^[-R+ ?][K NMD!][* ] ', line ): continue
225
 
                        line = line.rstrip()
226
 
 
227
 
                        # renames show before and after file names
228
 
                        matches = re.search( '^R.. (.*?)[/@+]? => (.*?)[/@+]?$', line )
229
 
                        if matches:
230
 
                                files.append( matches.group( 1 ) )
231
 
                                files.append( matches.group( 2 ) )
232
 
                                continue
233
 
 
234
 
                        # kind changes shows the same name twice
235
 
                        matches = re.search( '^.K. (.*?)[/@+]? => (.*?)[/@+]?$', line )
236
 
                        if matches:
237
 
                                files.append( matches.group( 1 ) )
238
 
                                continue
239
 
 
240
 
                        # other entries have only one filename
241
 
                        matches = re.search( '^... (.*?)[/@+]?$', line )
242
 
                        if matches:
243
 
                                files.append( matches.group( 1 ) )
244
 
                                continue
245
 
 
246
 
                        raise RuntimeError(
247
 
                                'failed to parse bzr update output line:\n%s' % line )
248
 
 
249
 
                return files
 
120
                # bzr update
 
121
                p = Popen( [ 'bzr', 'update' ], cwd = self.dir,
 
122
                                   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
 
123
                output = p.communicate()[ 0 ]
 
124
                if p.returncode > 0:
 
125
                        raise the.program.FatalError( 'bzr update failed', output )
250
126
 
251
127
 
252
128
        def has_changes( self ):
254
130
                """
255
131
 
256
132
                # bzr status
257
 
                output = self.run( [ 'bzr', 'status', '--no-pending' ] )
258
 
 
259
 
                # parse output
 
133
                p = Popen( [ 'bzr', 'status', '--no-pending' ], cwd = self.dir,
 
134
                                   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
 
135
                output = p.communicate()[ 0 ]
 
136
                if p.returncode > 0:
 
137
                        raise the.program.FatalError( 'bzr status failed', output )
260
138
                files = self.parse_file_blocks( output )
261
139
                return True if len( files ) else False
262
140
 
266
144
                """
267
145
 
268
146
                # bzr status
269
 
                output = self.run( [ 'bzr', 'status', '--no-pending' ] )
270
 
 
271
 
                # parse output
 
147
                p = Popen( [ 'bzr', 'status', '--no-pending' ], cwd = self.dir,
 
148
                                   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
 
149
                output = p.communicate()[ 0 ]
 
150
                if p.returncode > 0:
 
151
                        raise the.program.FatalError( 'bzr status failed', output )
272
152
                files = self.parse_file_blocks( output )
273
153
                return files['conflicts'] if 'conflicts' in files else None
274
154
 
275
155
 
276
 
        def add( self, files ):
277
 
                """Make sure files are added to version control.
278
 
                @param files a list of relative filenames
279
 
                """
280
 
 
281
 
                # bzr add
282
 
                self.run( [ 'bzr', 'add', '-N' ] + files )
283
 
 
284
 
 
285
 
        def commit( self ):
286
 
                """Commit changes to the repo.
287
 
                """
288
 
 
289
 
                # bzr commit
290
 
                try:
291
 
                        self.run( [ 'bzr', 'commit', '-m', '' ] )
292
 
                except self.VcsError as e:
293
 
                        if re.search( 'Working tree is out of date', e.output ):
294
 
                                raise the.program.FatalError(
295
 
                                        'you must update your files first.\n' +
296
 
                                        'Hint: see "%s update --help"' % the.program.name );
297
 
                        else:
298
 
                                raise e
299
 
 
300
 
 
301
 
        def run( self, cmd ):
302
 
                if the.verbose >= 2: print 'exec: %s' % ' '.join( cmd )
303
 
                p = Popen( cmd, cwd = self.dir,
304
 
                                   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
305
 
                output = p.communicate()[ 0 ]
306
 
                if p.returncode > 0:
307
 
                        raise self.VcsError( ' '.join( cmd[ : 2 ] ), output )
308
 
                if the.verbose >= 2:
309
 
                        verbose_output = output.rstrip()
310
 
                        if len( verbose_output ):
311
 
                                print re.sub( '(^|\n)', '\\1  : ', verbose_output )
312
 
                return output
313
 
 
314
 
 
315
156
        def parse_file_blocks( self, output ):
316
157
                res = dict()
317
158
                current = None
328
169
                                                res[ current ] = list()
329
170
                                        res[ current ].append( matches.group( 1 ) )
330
171
                                        continue
331
 
                        if re.search( '^[0-9]+ shel(?:f|ves) exists?', line ): continue
 
172
                        if re.search( '^[0-9]+ shelf exists', line ): continue
332
173
                        if re.search( '^working tree is out of date', line ): continue
333
174
                        raise self.ParseError( "unrecognised line: %s" % line )
334
175
                return res