/stdhome

To get this branch, use:
bzr branch http://bzr.ed.am/stdhome
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
1
# bzr.py
2
#
3
# Copyright (C) 2014 Tim Marston <tim@edm.am>
4
#
5
# This file is part of stdhome (hereafter referred to as "this program").
6
# See http://ed.am/dev/stdhome for more information.
7
#
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
12
#
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
# GNU General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License
19
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21
22
import subprocess, os, re, shutil
23
from subprocess import Popen
24
import StringIO
5 by Tim Marston
moved copy-in, copy-out and deployment conflict checking to a set of "walkers";
25
from vcs import Vcs
26
from stdhome import the
27
28
8 by Tim Marston
added diff command; moved all command to commands subdir; made stage-revert
29
class BzrVcs( Vcs ):
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
30
31
32
	def __init__( self, dir ):
33
		"""Init class
34
35
		@param dir the fully-qualified directory to work in.
36
		"""
37
		self.dir = dir
38
39
40
	def init( self ):
41
		"""Create a new, empty branch
42
		"""
43
44
		# the directory shouldn't exist
45
		os.mkdir( self.dir )
46
47
		# bzr init
8 by Tim Marston
added diff command; moved all command to commands subdir; made stage-revert
48
		try:
49
			self.run( [ 'bzr', 'init', '.' ] )
50
		except self.VcsError as e:
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
51
52
			# attempt to clean-up dir
53
			try:
54
				shutil.rmtree( self.dir )
55
			except OSError:
56
				pass
57
8 by Tim Marston
added diff command; moved all command to commands subdir; made stage-revert
58
			raise
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
59
60
61
	def checkout( self, url ):
62
		"""Checkout a new copy of a remote branch.
63
64
		@param url the remote repository URL
65
		"""
66
67
		# the directory shouldn't exist
68
		os.mkdir( self.dir )
69
70
		# bzr co
8 by Tim Marston
added diff command; moved all command to commands subdir; made stage-revert
71
		try:
72
			self.run( [ 'bzr', 'checkout', url, '.' ] )
73
		except self.VcsError as e:
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
74
75
			# attempt to clean-up dir
76
			try:
77
				shutil.rmtree( self.dir )
78
			except OSError:
79
				pass
80
8 by Tim Marston
added diff command; moved all command to commands subdir; made stage-revert
81
			raise
82
83
84
	def get_revno( self ):
85
		"""Obtain some sort of revision identifier
86
		"""
87
88
		# bzr revert
89
		output = self.run( [ 'bzr', 'revno', '--tree' ] )
90
91
		# parse revno
92
		buf = StringIO.StringIO( output )
93
		return buf.readline().rstrip()
94
95
96
	def revert( self, revno = None ):
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
97
		"""Revert the branch so that there are no outstanding changes or unknown files.
8 by Tim Marston
added diff command; moved all command to commands subdir; made stage-revert
98
		If a revno is supplied, then the repository is reverted to that
99
		revision.
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
100
		"""
101
26 by Tim Marston
fixed more bugs in the bzr backend
102
		# bzr st
103
		output = self.run( [ 'bzr', 'status' ] )
104
		files = self.parse_file_blocks( output )
105
106
		# remove kind changed files (or they can cause `bzr revert` to break in
107
		# strange situations, like when a directory has been replaced with a
108
		# symlink to a non-existant file)
109
		if 'kind changed' in files:
110
			for file in files[ 'kind changed' ]:
111
				matches = re.match( r'(.+?)[/@+]? \([^)]+\)', file )
112
				if not matches:
113
					raise RunTimeError(
114
						'failed to parse bzr kind change: %s' % file )
115
				file = matches.group( 1 )
32 by Tim Marston
make verbose levels clearer
116
				if the.verbose >= 2: print "removing (kind changed): " + file
26 by Tim Marston
fixed more bugs in the bzr backend
117
				full_file = os.path.join( self.dir, file )
118
				if os.path.isfile( full_file ) or os.path.islink( full_file ):
119
					os.unlink( full_file )
120
				elif os.path.isdir( full_file ):
121
					shutil.rmtree( full_file )
122
				else:
123
					raise RuntimeError( 'exotic file in repo: %s' % file )
124
125
		# bzr revert
8 by Tim Marston
added diff command; moved all command to commands subdir; made stage-revert
126
		self.run( [ 'bzr', 'revert', '--no-backup' ] )
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
127
128
		# bzr st
8 by Tim Marston
added diff command; moved all command to commands subdir; made stage-revert
129
		output = self.run( [ 'bzr', 'status' ] )
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
130
		files = self.parse_file_blocks( output )
131
132
		# remove unknown files
133
		if 'unknown' in files:
134
			for file in files[ 'unknown' ]:
32 by Tim Marston
make verbose levels clearer
135
				if the.verbose >= 2: print "removing (unknown): " + file
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
136
				full_file = os.path.join( self.dir, file )
137
				if os.path.isfile( full_file ):
138
					os.unlink( full_file )
22 by Tim Marston
fixed some bugs in Bzr.revert()
139
				elif os.path.isdir( full_file ):
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
140
					shutil.rmtree( full_file )
141
				else:
142
					raise RuntimeError( 'exotic file in repo: %s' % file )
143
8 by Tim Marston
added diff command; moved all command to commands subdir; made stage-revert
144
		# if a revision identifyer has been given, update to that
145
		if revno is not None:
146
147
			# bzr update
148
			self.run( [ 'bzr', 'update', '-r', revno ] )
149
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
150
151
	def update( self ):
5 by Tim Marston
moved copy-in, copy-out and deployment conflict checking to a set of "walkers";
152
		"""Update the branch, pulling down any upstream changes and merging them.  This
153
		method returns a list of the files that were modified as part of this
154
		operation.
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
155
		"""
156
8 by Tim Marston
added diff command; moved all command to commands subdir; made stage-revert
157
#		WARNING: the following might cause bzr to ask for your ssh password more than
158
#		once during an update!!!
159
#
160
#		# get revno
161
#		revno = self.get_revno()
162
#
163
#		# update to current revision (pull in history without updating tree)
164
#		self.run( [ 'bzr', 'update', '-r', revno ] )
165
#
166
#		# get log output
167
#		next_revno = str( int( revno ) + 1 )
168
#		output = self.run( [ 'bzr', 'log', '-r', next_revno + '..' ] )
169
#
170
#		# parse output
171
#		keep_files = list()
172
#		buf = StringIO.StringIO( output )
173
#		in_message = False
174
#		for line in buf:
175
#			line = line.rstrip( '\n' )
176
#			if line.lower() == 'message:':
177
#				in_message = True
178
#			elif in_message:
179
#				if line[ : 2 ] != '  ':
180
#					in_message = False
181
#				else:
182
#					line = line[ 2 : ]
183
#
184
#					# process directives
185
#					if line[ : 6 ].lower() == 'keep: ':
186
#						file = line[ 6 : ]
187
#						if file in rename_files: file = rename_files[ file ]
188
#						keep_files.append( file )
189
#					elif line[ : 8 ].lower() == 'rename: ':
190
#						rename_from = line[ 8 : ]
191
#					elif line[ : 4 ].lower() == 'to: ':
192
#						if rename_from in rename_files:
193
#							rename_from = rename_files[ rename_from ]
194
#						rename_files[ line[ 4 : ] ] = rename_from
195
196
		# bzr update properly
197
		output = self.run( [ 'bzr', 'update' ] )
5 by Tim Marston
moved copy-in, copy-out and deployment conflict checking to a set of "walkers";
198
199
		# parse output (see logic in report() in bzrlib/delta.py)
200
		files = list()
201
		buf = StringIO.StringIO( output )
202
		for line in buf:
203
			if not re.search( '^[-R+ ?][K NMD!][* ] ', line ): continue
204
			line = line.rstrip()
32 by Tim Marston
make verbose levels clearer
205
			if the.verbose >= 2: print '  %s' % line
5 by Tim Marston
moved copy-in, copy-out and deployment conflict checking to a set of "walkers";
206
207
			# renames show before and after file names
208
			matches = re.search( '^R.. (.*?)[/@+]? => (.*?)[/@+]?$', line )
209
			if matches:
210
				files.append( matches.group( 1 ) )
211
				files.append( matches.group( 2 ) )
212
				continue
213
214
			# kind changes shows the same name twice
215
			matches = re.search( '^.K. (.*?)[/@+]? => (.*?)[/@+]?$', line )
216
			if matches:
217
				files.append( matches.group( 1 ) )
218
				continue
219
220
			# other entries have only one filename
221
			matches = re.search( '^... (.*?)[/@+]?$', line )
222
			if matches:
223
				files.append( matches.group( 1 ) )
224
				continue
225
226
			raise RuntimeError(
26 by Tim Marston
fixed more bugs in the bzr backend
227
				'failed to parse bzr update output line:\n%s' % line )
5 by Tim Marston
moved copy-in, copy-out and deployment conflict checking to a set of "walkers";
228
229
		return files
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
230
231
232
	def has_changes( self ):
233
		"""Check if the branch has any local modifications.
234
		"""
235
236
		# bzr status
8 by Tim Marston
added diff command; moved all command to commands subdir; made stage-revert
237
		output = self.run( [ 'bzr', 'status', '--no-pending' ] )
238
239
		# parse output
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
240
		files = self.parse_file_blocks( output )
241
		return True if len( files ) else False
242
243
244
	def get_conflicts( self ):
245
		"""Return a list of files that have conflicts.
246
		"""
247
248
		# bzr status
8 by Tim Marston
added diff command; moved all command to commands subdir; made stage-revert
249
		output = self.run( [ 'bzr', 'status', '--no-pending' ] )
250
251
		# parse output
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
252
		files = self.parse_file_blocks( output )
253
		return files['conflicts'] if 'conflicts' in files else None
254
255
8 by Tim Marston
added diff command; moved all command to commands subdir; made stage-revert
256
	def run( self, cmd ):
32 by Tim Marston
make verbose levels clearer
257
		if the.verbose >= 2: print 'exec: %s' % ' '.join( cmd )
8 by Tim Marston
added diff command; moved all command to commands subdir; made stage-revert
258
		p = Popen( cmd, cwd = self.dir,
259
				   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
260
		output = p.communicate()[ 0 ]
261
		if p.returncode > 0:
262
			raise self.VcsError( ' '.join( cmd[ : 2 ] ), output )
263
		return output
264
265
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
266
	def parse_file_blocks( self, output ):
267
		res = dict()
268
		current = None
269
		buf = StringIO.StringIO( output )
270
		for line in buf:
271
			matches = re.search( '^([a-z ]+):$', line, re.I )
272
			if matches:
273
				current = matches.group( 1 )
274
				continue
275
			if current:
276
				matches = re.search( '^  ([^ ].*)$', line )
277
				if matches:
278
					if not current in res:
279
						res[ current ] = list()
280
					res[ current ].append( matches.group( 1 ) )
281
					continue
26 by Tim Marston
fixed more bugs in the bzr backend
282
			if re.search( '^[0-9]+ shel(?:f|ves) exists?', line ): continue
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
283
			if re.search( '^working tree is out of date', line ): continue
284
			raise self.ParseError( "unrecognised line: %s" % line )
285
		return res
286
287
288
	class ParseError( Exception ):
289
		pass