/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
29
class VcsBzr( 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
48
		p = Popen( [ 'bzr', 'init', '.' ], cwd = self.dir,
49
				   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
50
		output = p.communicate()[ 0 ]
51
		if p.returncode > 0:
52
53
			# attempt to clean-up dir
54
			try:
55
				shutil.rmtree( self.dir )
56
			except OSError:
57
				pass
58
5 by Tim Marston
moved copy-in, copy-out and deployment conflict checking to a set of "walkers";
59
			raise self.VcsError( 'bzr init failed', output )
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
60
61
62
	def checkout( self, url ):
63
		"""Checkout a new copy of a remote branch.
64
65
		@param url the remote repository URL
66
		"""
67
68
		# the directory shouldn't exist
69
		os.mkdir( self.dir )
70
71
		# bzr co
72
		p = Popen( [ 'bzr', 'co', url, '.' ], cwd = self.dir,
73
				   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
74
		output = p.communicate()[ 0 ]
75
		if p.returncode > 0:
76
77
			# attempt to clean-up dir
78
			try:
79
				shutil.rmtree( self.dir )
80
			except OSError:
81
				pass
82
5 by Tim Marston
moved copy-in, copy-out and deployment conflict checking to a set of "walkers";
83
			raise self.VcsError( 'bzr checkout failed', output )
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
84
85
86
	def revert( self ):
87
		"""Revert the branch so that there are no outstanding changes or unknown files.
88
		"""
89
90
		# bzr revert
91
		p = Popen( [ 'bzr', 'revert', '--no-backup' ], cwd = self.dir,
92
				   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
93
		output = p.communicate()[ 0 ]
94
		if p.returncode > 0:
5 by Tim Marston
moved copy-in, copy-out and deployment conflict checking to a set of "walkers";
95
			raise self.VcsError( 'bzr revert failed', output )
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
96
97
		# bzr st
98
		p = Popen( [ 'bzr', 'st' ], cwd = self.dir,
99
				   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
100
		output = p.communicate()[ 0 ]
101
		if p.returncode > 0:
5 by Tim Marston
moved copy-in, copy-out and deployment conflict checking to a set of "walkers";
102
			raise self.VcsError( 'bzr status failed', output )
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
103
		files = self.parse_file_blocks( output )
104
105
		# remove unknown files
106
		if 'unknown' in files:
107
			for file in files[ 'unknown' ]:
108
				full_file = os.path.join( self.dir, file )
109
				if os.path.isfile( full_file ):
110
					os.unlink( full_file )
111
				elif os.full_file.isdir( full_file ):
112
					shutil.rmtree( full_file )
113
				else:
114
					raise RuntimeError( 'exotic file in repo: %s' % file )
115
116
117
	def update( self ):
5 by Tim Marston
moved copy-in, copy-out and deployment conflict checking to a set of "walkers";
118
		"""Update the branch, pulling down any upstream changes and merging them.  This
119
		method returns a list of the files that were modified as part of this
120
		operation.
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
121
		"""
122
123
		# bzr update
124
		p = Popen( [ 'bzr', 'update' ], cwd = self.dir,
125
				   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
126
		output = p.communicate()[ 0 ]
127
		if p.returncode > 0:
5 by Tim Marston
moved copy-in, copy-out and deployment conflict checking to a set of "walkers";
128
			raise self.VcsError( 'bzr update failed', output )
129
130
		# parse output (see logic in report() in bzrlib/delta.py)
131
		files = list()
132
		buf = StringIO.StringIO( output )
133
		for line in buf:
134
			if not re.search( '^[-R+ ?][K NMD!][* ] ', line ): continue
135
			line = line.rstrip()
136
			if the.verbose: print '  %s' % line
137
138
			# renames show before and after file names
139
			matches = re.search( '^R.. (.*?)[/@+]? => (.*?)[/@+]?$', line )
140
			if matches:
141
				files.append( matches.group( 1 ) )
142
				files.append( matches.group( 2 ) )
143
				continue
144
145
			# kind changes shows the same name twice
146
			matches = re.search( '^.K. (.*?)[/@+]? => (.*?)[/@+]?$', line )
147
			if matches:
148
				files.append( matches.group( 1 ) )
149
				continue
150
151
			# other entries have only one filename
152
			matches = re.search( '^... (.*?)[/@+]?$', line )
153
			if matches:
154
				files.append( matches.group( 1 ) )
155
				continue
156
157
			raise RuntimeError(
158
				'failed to parse bzr update output line:\n%' % line )
159
160
		return files
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
161
162
163
	def has_changes( self ):
164
		"""Check if the branch has any local modifications.
165
		"""
166
167
		# bzr status
168
		p = Popen( [ 'bzr', 'status', '--no-pending' ], cwd = self.dir,
169
				   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
170
		output = p.communicate()[ 0 ]
171
		if p.returncode > 0:
5 by Tim Marston
moved copy-in, copy-out and deployment conflict checking to a set of "walkers";
172
			raise self.VcsError( 'bzr status failed', output )
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
173
		files = self.parse_file_blocks( output )
174
		return True if len( files ) else False
175
176
177
	def get_conflicts( self ):
178
		"""Return a list of files that have conflicts.
179
		"""
180
181
		# bzr status
182
		p = Popen( [ 'bzr', 'status', '--no-pending' ], cwd = self.dir,
183
				   stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
184
		output = p.communicate()[ 0 ]
185
		if p.returncode > 0:
5 by Tim Marston
moved copy-in, copy-out and deployment conflict checking to a set of "walkers";
186
			raise self.VcsError( 'bzr status failed', output )
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
187
		files = self.parse_file_blocks( output )
188
		return files['conflicts'] if 'conflicts' in files else None
189
190
191
	def parse_file_blocks( self, output ):
192
		res = dict()
193
		current = None
194
		buf = StringIO.StringIO( output )
195
		for line in buf:
196
			matches = re.search( '^([a-z ]+):$', line, re.I )
197
			if matches:
198
				current = matches.group( 1 )
199
				continue
200
			if current:
201
				matches = re.search( '^  ([^ ].*)$', line )
202
				if matches:
203
					if not current in res:
204
						res[ current ] = list()
205
					res[ current ].append( matches.group( 1 ) )
206
					continue
207
			if re.search( '^[0-9]+ shelf exists', line ): continue
208
			if re.search( '^working tree is out of date', line ): continue
209
			raise self.ParseError( "unrecognised line: %s" % line )
210
		return res
211
212
213
	class ParseError( Exception ):
214
		pass