Mercurial > hgsubversion
annotate tests/fixtures/rsvn.py @ 1233:0d0132cba155
editor: fix edge case with in memory file-store size limit
There are a few cases where we will set a single file into to the
editor's FileStore object more than once. Notably, for copied and
then modified files, we will set it at least twice. Three times if
editing fails (which it can for symlinks).
If we pass the in-memory storage limit in between the first (or second
if editing fails) time we set the file and the last time we set the
file, we will write the data to the in memory store the first time and
the file store the last time. We didn't remove it form the in-memory
store though, and we always prefer reading from the in-memory store.
This means we can sometimes end up with the wrong version of a file.
This is fairly unlikely to happen in normal use since you need to hit
the memory limit between two writes to the store for the same file.
We only write a file multiple times if a) the file (and not one of
it's parent directories) is copied and then modified or b) editing
fails. From what I can tell, it's only common for editing to fail for
symlinks, and they ten to be relatively small data that is unlikely to
push over the limit. Finally, the default limit is 100MB which I
would expect to be most often either well over (source code) or well
under (binaries or automated changes) the size of the changes files in
a single commit.
The easiest way to reproduce this is to set the in-memory cache size
to 0 and then commit a copied and modified symlink. The empty-string
version from the failed editing will be the one that persists. I
happened to stumble upon this while trying (and failing) to test a
bug-fix for a related bug with identical symptoms (empty simlink). I
have seen this in the wild, once, but couldn't reproduce it at the
time. The repo in question is quite large and quite active, so I am
quite confident in my estimation that this is a real, but very rare,
problem.
The test changes attached to this was mneant to test a related bug,
but turned out not to actually cover the bug in question. They did
trigger this bug though, and are worthwhile to test, so I kept them.
author | David Schleimer <dschleimer@fb.com> |
---|---|
date | Mon, 07 Apr 2014 17:51:59 -0700 |
parents | e9af7eba88db |
children |
rev | line source |
---|---|
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) | |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
172 |
581 | 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 | |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
184 |
581 | 185 if log_msg == None: |
186 usage('Missing --message argument') | |
187 sys.exit(1) | |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
188 |
581 | 189 if len(args) != 1: |
190 usage('Missing repository path argument') | |
191 sys.exit(1) | |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
192 |
581 | 193 repos_path = args[0] |
194 print 'Accessing repository at [%s]' % repos_path | |
195 | |
196 repository = Repository(repos_path, pool) | |
197 sub = repository.subpool() | |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
198 |
581 | 199 try: |
200 txn = repository.begin(username, log_msg) | |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
201 |
581 | 202 # Read commands from STDIN |
203 lineno = 0 | |
204 for line in sys.stdin: | |
205 lineno += 1 | |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
206 |
581 | 207 core.svn_pool_clear(sub) |
208 try: | |
209 if COMMENT_RE.search(line): | |
210 continue | |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
211 |
581 | 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 | |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
218 |
581 | 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 | |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
225 |
581 | 226 match = RMKDIR_RE.search(line) |
227 if match: | |
228 entry = match.group(1) | |
229 txn.mkdir(entry, sub) | |
230 continue | |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
231 |
581 | 232 match = RDELETE_RE.search(line) |
233 if match: | |
234 entry = match.group(1) | |
235 txn.delete(entry, sub) | |
236 continue | |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
237 |
581 | 238 raise NameError, ('Unknown command [%s] on line %d' % |
239 (line, lineno)) | |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
240 |
581 | 241 except: |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
242 sys.stderr.write(('Exception occured while processing line %d:\n' % |
581 | 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) | |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
249 |
581 | 250 new_rev = txn.commit() |
251 print '\nCommitted revision %d.' % new_rev | |
832
e9af7eba88db
globally: clean up whitespace around operators and commas to conform with PEP8
Yonggang Luo <luoyonggang@gmail.com>
parents:
581
diff
changeset
|
252 |
581 | 253 finally: |
254 print '\nRepository closed.' | |
255 | |
256 def main(): | |
257 core.run_app(rsvn) | |
258 | |
259 if __name__ == '__main__': | |
260 main() |