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