581
|
1 #!/usr/bin/env python2 |
|
2 |
|
3 # LICENSE |
|
4 # |
|
5 # Copyright (c) 2004, Francois Beausoleil |
|
6 # All rights reserved. |
|
7 # |
|
8 # Redistribution and use in source and binary forms, with or without |
|
9 # modification, are permitted provided that the following conditions |
|
10 # are met: |
|
11 # |
|
12 # * Redistributions of source code must retain the above copyright |
|
13 # notice, this list of conditions and the following disclaimer. |
|
14 # * Redistributions in binary form must reproduce the above copyright |
|
15 # notice, this list of conditions and the following disclaimer in |
|
16 # the documentation and/or other materials provided with the |
|
17 # distribution. |
|
18 # * Neither the name of the Francois Beausoleil nor the names of its |
|
19 # contributors may be used to endorse or promote products derived |
|
20 # from this software without specific prior written permission. |
|
21 # |
|
22 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|
23 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|
24 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|
25 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
|
26 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
27 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
28 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
|
29 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
|
30 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
31 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
|
32 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
33 |
|
34 import getopt |
|
35 import sys |
|
36 import os |
|
37 import re |
|
38 import traceback |
|
39 from svn import core, repos, fs |
|
40 |
|
41 VERSION='$Id: rsvn.py 20 2005-09-23 15:08:08Z fbos $' |
|
42 RCOPY_RE = re.compile('^\s*rcopy\s+(.+)\s+(.+)$') |
|
43 RMOVE_RE = re.compile('^\s*rmove\s+(.+)\s+(.+)$') |
|
44 RMKDIR_RE = re.compile('^\s*rmkdir\s+(.+)$') |
|
45 RDELETE_RE = re.compile('^\s*rdelete\s+(.+)$') |
|
46 COMMENT_RE = re.compile('(?:^\s*#)|(?:^\s*$)') |
|
47 |
|
48 def usage(error=None): |
|
49 if error: |
|
50 print 'Error: %s\n' % error |
|
51 print 'USAGE: %s --message=MESSAGE repos_path [--username=USERNAME]' % ( |
|
52 os.path.basename(sys.argv[0])) |
|
53 print '' |
|
54 print ' --help, -h print this usage message and exit with success' |
|
55 print ' --version print the version number' |
|
56 print ' --username=USERNAME Username to execute the commands under' |
|
57 print ' --message=LOG_MSG Log message to execute the commit with' |
|
58 print '' |
|
59 print 'Reads STDIN and parses the following commands, to execute them on the server, ' |
|
60 print 'all within the same transaction:' |
|
61 print '' |
|
62 print ' rcopy SRC DEST Copy the HEAD revision of a file or folder' |
|
63 print ' rmove SRC DEST Copy + delete the HEAD revision of a file or folder' |
|
64 print ' rdelete TARGET Deletes something from the repository' |
|
65 print ' rmkdir TARGET Creates a new folder (must create parents first)' |
|
66 print ' # Initiates a comment' |
|
67 print '' |
|
68 # 12345678901234567890123456789012345678901234567890123456789012345678901234567890 |
|
69 |
|
70 |
|
71 class Transaction: |
|
72 """Represents a transaction in a Subversion repository |
|
73 |
|
74 Transactions are long-lived objects which exist in the repository, |
|
75 and are used to build an intermediate representation of a new |
|
76 revision. Once the transaction is committed, the repository |
|
77 bumps the revision number, and links the new transaction in the |
|
78 Subversion filesystem.""" |
|
79 |
|
80 def __init__(self, repository, rev, username, message, pool, logger=None): |
|
81 if logger: |
|
82 self.logger = logger |
|
83 else: |
|
84 self.logger = sys.stdout |
|
85 self.pool = pool |
|
86 self.rev = rev |
|
87 |
|
88 self.fsptr = repos.svn_repos_fs(repository) |
|
89 self.rev_root = fs.revision_root(self.fsptr, self.rev, |
|
90 self.pool) |
|
91 self.txnp = repos.svn_repos_fs_begin_txn_for_commit( |
|
92 repository, self.rev, username, message, self.pool) |
|
93 self.txn_root = fs.txn_root(self.txnp, self.pool) |
|
94 self.log('Base revision %d\n' % rev) |
|
95 |
|
96 def commit(self): |
|
97 values = fs.commit_txn(self.txnp, self.pool) |
|
98 return values[1] |
|
99 |
|
100 def rollback(self): |
|
101 fs.abort_txn(self.txnp, self.pool) |
|
102 |
|
103 def copy(self, src, dest, subpool): |
|
104 self.log('A + %s\n' % dest) |
|
105 fs.copy(self.rev_root, src, self.txn_root, dest, subpool) |
|
106 |
|
107 def delete(self, entry, subpool): |
|
108 self.log('D %s\n' % entry) |
|
109 fs.delete(self.txn_root, entry, subpool) |
|
110 |
|
111 def mkdir(self, entry, subpool): |
|
112 self.log('A %s\n' % entry) |
|
113 fs.make_dir(self.txn_root, entry, subpool) |
|
114 |
|
115 def move(self, src, dest, subpool): |
|
116 self.copy(src, dest, subpool) |
|
117 self.delete(src, subpool) |
|
118 |
|
119 def log(self, msg): |
|
120 self.logger.write(msg) |
|
121 |
|
122 |
|
123 class Repository: |
|
124 """Represents a Subversion repository, and allows common operations |
|
125 on it.""" |
|
126 |
|
127 def __init__(self, repos_path, pool, logger=None): |
|
128 if logger: |
|
129 self.logger = logger |
|
130 else: |
|
131 self.logger = sys.stdout |
|
132 self.pool = pool |
|
133 assert self.pool |
|
134 |
|
135 self.repo = repos.svn_repos_open(repos_path, self.pool) |
|
136 self.fsptr = repos.svn_repos_fs(self.repo) |
|
137 |
|
138 def get_youngest(self): |
|
139 """Returns the youngest revision in the repository.""" |
|
140 return fs.youngest_rev(self.fsptr, self.pool) |
|
141 |
|
142 def begin(self, username, log_msg): |
|
143 """Initiate a new Transaction""" |
|
144 return Transaction(self.repo, self.get_youngest(), username, |
|
145 log_msg, self.pool, self.logger) |
|
146 |
|
147 def close(self): |
|
148 """Close the repository, aborting any uncommitted transactions""" |
|
149 core.svn_pool_destroy(self.pool) |
|
150 core.apr_terminate() |
|
151 |
|
152 def subpool(self): |
|
153 """Instantiates a new pool from the master pool""" |
|
154 return core.svn_pool_create(self.pool) |
|
155 |
|
156 def delete_pool(self, pool): |
|
157 """Deletes the passed-in pool. Returns None, to assign to pool in |
|
158 caller.""" |
|
159 core.svn_pool_destroy(pool) |
|
160 return None |
|
161 |
|
162 def rsvn(pool): |
|
163 log_msg = None |
|
164 |
|
165 try: |
|
166 opts, args = getopt.getopt(sys.argv[1:], 'vh', |
|
167 ["help", "username=", "message=", "version"]) |
|
168 except getopt.GetoptError, e: |
|
169 sys.stderr.write(str(e) + '\n\n') |
|
170 usage() |
|
171 sys.exit(1) |
|
172 |
|
173 for opt, value in opts: |
|
174 if opt == '--version': |
|
175 print '%s version %s' % (os.path.basename(sys.argv[0]), VERSION) |
|
176 sys.exit(0) |
|
177 elif opt == '--help' or opt == '-h': |
|
178 usage() |
|
179 sys.exit(0) |
|
180 elif opt == '--username': |
|
181 username = value |
|
182 elif opt == '--message': |
|
183 log_msg = value |
|
184 |
|
185 if log_msg == None: |
|
186 usage('Missing --message argument') |
|
187 sys.exit(1) |
|
188 |
|
189 if len(args) != 1: |
|
190 usage('Missing repository path argument') |
|
191 sys.exit(1) |
|
192 |
|
193 repos_path = args[0] |
|
194 print 'Accessing repository at [%s]' % repos_path |
|
195 |
|
196 repository = Repository(repos_path, pool) |
|
197 sub = repository.subpool() |
|
198 |
|
199 try: |
|
200 txn = repository.begin(username, log_msg) |
|
201 |
|
202 # Read commands from STDIN |
|
203 lineno = 0 |
|
204 for line in sys.stdin: |
|
205 lineno += 1 |
|
206 |
|
207 core.svn_pool_clear(sub) |
|
208 try: |
|
209 if COMMENT_RE.search(line): |
|
210 continue |
|
211 |
|
212 match = RCOPY_RE.search(line) |
|
213 if match: |
|
214 src = match.group(1) |
|
215 dest = match.group(2) |
|
216 txn.copy(src, dest, sub) |
|
217 continue |
|
218 |
|
219 match = RMOVE_RE.search(line) |
|
220 if match: |
|
221 src = match.group(1) |
|
222 dest = match.group(2) |
|
223 txn.move(src, dest, sub) |
|
224 continue |
|
225 |
|
226 match = RMKDIR_RE.search(line) |
|
227 if match: |
|
228 entry = match.group(1) |
|
229 txn.mkdir(entry, sub) |
|
230 continue |
|
231 |
|
232 match = RDELETE_RE.search(line) |
|
233 if match: |
|
234 entry = match.group(1) |
|
235 txn.delete(entry, sub) |
|
236 continue |
|
237 |
|
238 raise NameError, ('Unknown command [%s] on line %d' % |
|
239 (line, lineno)) |
|
240 |
|
241 except: |
|
242 sys.stderr.write(('Exception occured while processing line %d:\n' % |
|
243 lineno)) |
|
244 etype, value, tb = sys.exc_info() |
|
245 traceback.print_exception(etype, value, tb, None, sys.stderr) |
|
246 sys.stderr.write('\n') |
|
247 txn.rollback() |
|
248 sys.exit(1) |
|
249 |
|
250 new_rev = txn.commit() |
|
251 print '\nCommitted revision %d.' % new_rev |
|
252 |
|
253 finally: |
|
254 print '\nRepository closed.' |
|
255 |
|
256 def main(): |
|
257 core.run_app(rsvn) |
|
258 |
|
259 if __name__ == '__main__': |
|
260 main() |