Merge upstream fixes.
[jelmer/subvertpy.git] / checkout.py
1 # Copyright (C) 2005-2006 Jelmer Vernooij <jelmer@samba.org>
2
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
17 from binascii import hexlify
18 from bzrlib.bzrdir import BzrDirFormat, BzrDir
19 from bzrlib.errors import NotBranchError, NoSuchFile, InvalidRevisionId
20 from bzrlib.inventory import (Inventory, InventoryDirectory, InventoryFile)
21 from bzrlib.lockable_files import TransportLock, LockableFiles
22 from bzrlib.lockdir import LockDir
23 from bzrlib.osutils import rand_bytes, fingerprint_file
24 from bzrlib.progress import DummyProgress
25 from bzrlib.revision import NULL_REVISION
26 from bzrlib.trace import mutter
27 from bzrlib.transport.local import LocalTransport
28 from bzrlib.workingtree import WorkingTree, WorkingTreeFormat
29
30 from branch import SvnBranch
31 from repository import (SvnRepository, escape_svn_path, SVN_PROP_BZR_MERGE,
32                         SVN_PROP_SVK_MERGE, revision_id_to_svk_feature) 
33 from scheme import BranchingScheme
34 from transport import (SvnRaTransport, svn_config, bzr_to_svn_url) 
35 from tree import SvnBasisTree
36
37 import os
38
39 import svn.core, svn.wc
40 from svn.core import SubversionException
41
42 class SvnWorkingTree(WorkingTree):
43     """Implementation of WorkingTree that uses a Subversion 
44     Working Copy for storage."""
45     def __init__(self, bzrdir, local_path, branch):
46         self._format = SvnWorkingTreeFormat()
47         self.basedir = local_path
48         self.bzrdir = bzrdir
49         self._branch = branch
50         self.base_revnum = 0
51         self.client_ctx = svn.client.create_context()
52         self.client_ctx.log_msg_func2 = svn.client.svn_swig_py_get_commit_log_func
53         self.client_ctx.log_msg_baton2 = self.log_message_func
54
55         self._set_inventory(self.read_working_inventory(), dirty=False)
56
57         self.base_revid = branch.repository.generate_revision_id(
58                     self.base_revnum, branch.branch_path)
59         self.controldir = os.path.join(self.basedir, svn.wc.get_adm_dir(), 'bzr')
60         try:
61             os.makedirs(self.controldir)
62             os.makedirs(os.path.join(self.controldir, 'lock'))
63         except OSError:
64             pass
65         control_transport = bzrdir.transport.clone(os.path.join(svn.wc.get_adm_dir(), 'bzr'))
66         self._control_files = LockableFiles(control_transport, 'lock', LockDir)
67
68     def lock_write(self):
69         pass
70
71     def lock_read(self):
72         pass
73
74     def unlock(self):
75         pass
76
77     def get_ignore_list(self):
78         ignores = []
79
80         def dir_add(wc, prefix):
81             ignores.append(os.path.join(prefix, svn.wc.get_adm_dir()))
82             for pat in svn.wc.get_ignores(svn_config, wc):
83                 ignores.append(os.path.join(prefix, pat))
84
85             entries = svn.wc.entries_read(wc, False)
86             for entry in entries:
87                 if entry == "":
88                     continue
89
90                 if entries[entry].kind != svn.core.svn_node_dir:
91                     continue
92
93                 subprefix = os.path.join(prefix, entry)
94
95                 subwc = svn.wc.adm_open3(wc, self.abspath(subprefix), False, 0, None)
96                 try:
97                     dir_add(subwc, subprefix)
98                 finally:
99                     svn.wc.adm_close(subwc)
100
101         wc = self._get_wc()
102         try:
103             dir_add(wc, "")
104         finally:
105             svn.wc.adm_close(wc)
106
107         return ignores
108
109     def _write_inventory(self, inv):
110         pass
111
112     def is_ignored(self, filename):
113         if svn.wc.is_adm_dir(os.path.basename(filename)):
114             return True
115
116         (wc, name) = self._get_rel_wc(filename)
117         assert wc
118         try:
119             ignores = svn.wc.get_ignores(svn_config, wc)
120             from fnmatch import fnmatch
121             for pattern in ignores:
122                 if fnmatch(name, pattern):
123                     return True
124             return False
125         finally:
126             svn.wc.adm_close(wc)
127
128     def is_control_filename(self, path):
129         return svn.wc.is_adm_dir(path)
130
131     def remove(self, files, verbose=False, to_file=None):
132         wc = self._get_wc(write_lock=True)
133         try:
134             for file in files:
135                 svn.wc.delete2(self.abspath(file), wc, None, None, None)
136         finally:
137             svn.wc.adm_close(wc)
138
139     def _get_wc(self, relpath="", write_lock=False):
140         return svn.wc.adm_open3(None, self.abspath(relpath).rstrip("/"), write_lock, 0, None)
141
142     def _get_rel_wc(self, relpath, write_lock=False):
143         dir = os.path.dirname(relpath)
144         file = os.path.basename(relpath)
145         return (self._get_wc(dir, write_lock), file)
146
147     def move(self, from_paths, to_name):
148         revt = svn.core.svn_opt_revision_t()
149         revt.kind = svn.core.svn_opt_revision_working
150         to_wc = self._get_wc(to_name, write_lock=True)
151         try:
152             for entry in from_paths:
153                 svn.wc.copy(self.abspath(entry), to_wc, os.path.basename(entry), None, None)
154         finally:
155             svn.wc.adm_close(to_wc)
156
157         for entry in from_paths:
158             self.remove([entry])
159
160     def rename_one(self, from_rel, to_rel):
161         revt = svn.core.svn_opt_revision_t()
162         revt.kind = svn.core.svn_opt_revision_unspecified
163         (to_wc, to_file) = self._get_rel_wc(to_rel, write_lock=True)
164         try:
165             svn.wc.copy(self.abspath(from_rel), to_wc, to_file, None, None)
166             svn.wc.delete2(self.abspath(from_rel), to_wc, None, None, None)
167         finally:
168             svn.wc.adm_close(to_wc)
169
170     def read_working_inventory(self):
171         inv = Inventory()
172
173         def add_file_to_inv(relpath, id, revid, parent_id):
174             """Add a file to the inventory."""
175             file = InventoryFile(id, os.path.basename(relpath), parent_id)
176             file.revision = revid
177             try:
178                 data = fingerprint_file(open(self.abspath(relpath)))
179                 file.text_sha1 = data['sha1']
180                 file.text_size = data['size']
181                 file.executable = self.is_executable(id, relpath)
182                 inv.add(file)
183             except IOError:
184                 # Ignore non-existing files
185                 pass
186
187         def find_copies(url, relpath=""):
188             wc = self._get_wc(relpath)
189             entries = svn.wc.entries_read(wc, False)
190             for entry in entries.values():
191                 subrelpath = os.path.join(relpath, entry.name)
192                 if entry.name == "" or entry.kind != 'directory':
193                     if ((entry.copyfrom_url == url or entry.url == url) and 
194                         not (entry.schedule in (svn.wc.schedule_delete,
195                                                 svn.wc.schedule_replace))):
196                         yield os.path.join(
197                                 self.branch.branch_path.strip("/"), 
198                                 subrelpath)
199                 else:
200                     find_copies(subrelpath)
201             svn.wc.adm_close(wc)
202
203         def find_ids(entry):
204             relpath = entry.url[len(entry.repos):].strip("/")
205             assert entry.schedule in (svn.wc.schedule_normal, 
206                                       svn.wc.schedule_delete,
207                                       svn.wc.schedule_add,
208                                       svn.wc.schedule_replace)
209             if entry.schedule == svn.wc.schedule_normal:
210                 assert entry.revision >= 0
211                 # Keep old id
212                 mutter('stay: %r' % relpath)
213                 return self.branch.repository.path_to_file_id(entry.revision, 
214                         relpath)
215             elif entry.schedule == svn.wc.schedule_delete:
216                 return (None, None)
217             elif (entry.schedule == svn.wc.schedule_add or 
218                   entry.schedule == svn.wc.schedule_replace):
219                 # See if the file this file was copied from disappeared
220                 # and has no other copies -> in that case, take id of other file
221                 mutter('copies(%r): %r' % (relpath, list(find_copies(entry.copyfrom_url))))
222                 if entry.copyfrom_url and list(find_copies(entry.copyfrom_url)) == [relpath]:
223                     return self.branch.repository.path_to_file_id(entry.copyfrom_rev,
224                         entry.copyfrom_url[len(entry.repos):])
225                 return ("NEW-" + escape_svn_path(entry.url[len(entry.repos):].strip("/")), None)
226
227         def add_dir_to_inv(relpath, wc, parent_id):
228             entries = svn.wc.entries_read(wc, False)
229
230             entry = entries[""]
231             
232             (id, revid) = find_ids(entry)
233
234             if id is None:
235                 return
236
237             self.base_revnum = max(self.base_revnum, entry.revision)
238
239             # First handle directory itself
240             inv.add_path(relpath, 'directory', id, parent_id).revision = revid
241
242             for name in entries:
243                 if name == "":
244                     continue
245
246                 subrelpath = os.path.join(relpath, name)
247
248                 entry = entries[name]
249                 assert entry
250                 
251                 if entry.kind == svn.core.svn_node_dir:
252                     subwc = svn.wc.adm_open3(wc, self.abspath(subrelpath), 
253                                              False, 0, None)
254                     try:
255                         add_dir_to_inv(subrelpath, subwc, id)
256                     finally:
257                         svn.wc.adm_close(subwc)
258                 else:
259                     (subid, subrevid) = find_ids(entry)
260                     if subid:
261                         self.base_revnum = max(self.base_revnum, entry.revision)
262                         add_file_to_inv(subrelpath, subid, subrevid, id)
263
264         wc = self._get_wc() 
265         try:
266             add_dir_to_inv("", wc, None)
267         finally:
268             svn.wc.adm_close(wc)
269
270         return inv
271
272     def set_last_revision(self, revid):
273         mutter('setting last revision to %r' % revid)
274         if revid is None or revid == NULL_REVISION:
275             self.base_revid = revid
276             return
277
278         # TODO: Implement more efficient version
279         newrev = self.branch.repository.get_revision(revid)
280         newrevtree = self.branch.repository.revision_tree(revid)
281
282         def update_settings(wc, path):
283             id = newrevtree.inventory.path2id(path)
284             mutter("Updating settings for %r" % id)
285             (_, revnum) = self.branch.repository.parse_revision_id(
286                     newrevtree.inventory[id].revision)
287
288             svn.wc.process_committed2(self.abspath(path).rstrip("/"), wc, 
289                           False, revnum, 
290                           svn.core.svn_time_to_cstring(newrev.timestamp), 
291                           newrev.committer, None, False)
292
293             if newrevtree.inventory[id].kind != 'directory':
294                 return
295
296             entries = svn.wc.entries_read(wc, True)
297             for entry in entries:
298                 if entry == "":
299                     continue
300
301                 subwc = svn.wc.adm_open3(wc, os.path.join(self.basedir, path, entry), False, 0, None)
302                 try:
303                     update_settings(subwc, os.path.join(path, entry))
304                 finally:
305                     svn.wc.adm_close(subwc)
306
307         # Set proper version for all files in the wc
308         wc = self._get_wc(write_lock=True)
309         try:
310             update_settings(wc, "")
311         finally:
312             svn.wc.adm_close(wc)
313         self.base_revid = revid
314
315
316     def log_message_func(self, items, pool):
317         """ Simple log message provider for unit tests. """
318         return self._message
319
320     def commit(self, message=None, revprops=None, timestamp=None, timezone=None, committer=None, rev_id=None, allow_pointless=True, 
321             strict=False, verbose=False, local=False, reporter=None, config=None, specific_files=None):
322         assert timestamp is None
323         assert timezone is None
324         assert rev_id is None
325
326         if specific_files:
327             specific_files = [self.abspath(x).encode('utf8') for x in specific_files]
328         else:
329             specific_files = [self.basedir.encode('utf8')]
330
331         assert isinstance(message, basestring)
332         self._message = message
333
334         commit_info = svn.client.commit3(specific_files, True, False, self.client_ctx)
335
336         revid = self.branch.repository.generate_revision_id(
337                 commit_info.revision, self.branch.branch_path)
338
339         self.base_revid = revid
340         self.branch._revision_history.append(revid)
341
342         return revid
343
344     def add(self, files, ids=None):
345         assert isinstance(files, list)
346         wc = self._get_wc(write_lock=True)
347         try:
348             for f in files:
349                 try:
350                     svn.wc.add2(os.path.join(self.basedir, f), wc, None, 0, 
351                             None, None, None)
352                     if ids:
353                         svn.wc.prop_set2('bzr:fileid', ids.pop(), relpath, wc, 
354                                 False)
355                 except SubversionException, (_, num):
356                     if num == svn.core.SVN_ERR_ENTRY_EXISTS:
357                         continue
358                     elif num == svn.core.SVN_ERR_WC_PATH_NOT_FOUND:
359                         raise NoSuchFile(path=f)
360                     raise
361         finally:
362             svn.wc.adm_close(wc)
363
364     def basis_tree(self):
365         if self.base_revid is None or self.base_revid == NULL_REVISION:
366             return self.branch.repository.revision_tree(self.base_revid)
367
368         return SvnBasisTree(self, self.base_revid)
369
370     def pull(self, source, overwrite=False, stop_revision=None):
371         if stop_revision is None:
372             stop_revision = self.branch.last_revision()
373         rev = svn.core.svn_opt_revision_t()
374         rev.kind = svn.core.svn_opt_revision_number
375         rev.value.number = self.branch.repository.parse_revision_id(stop_revision)[1]
376         fetched = svn.client.update(self.basedir, rev, True, self.client_ctx)
377         self.base_revid = self.branch.repository.generate_revision_id(fetched, self.branch.branch_path)
378         return fetched-rev.value.number
379
380     def get_file_sha1(self, file_id, path=None, stat_value=None):
381         if not path:
382             path = self._inventory.id2path(file_id)
383
384         return fingerprint_file(open(self.abspath(path)))['sha1']
385
386     def _get_bzr_merges(self):
387         return self.branch.repository.branchprop_list.get_property(self.branch.branch_path, 
388                                             self.base_revnum, 
389                                             SVN_PROP_BZR_MERGE, "")
390
391     def _get_svk_merges(self):
392         return self.branch.repository.branchprop_list.get_property(self.branch.branch_path, 
393                                             self.base_revnum, 
394                                             SVN_PROP_SVK_MERGE, "")
395
396     def set_pending_merges(self, merges):
397         wc = self._get_wc(write_lock=True)
398         try:
399             # Set bzr:merge
400             if len(merges) > 0:
401                 bzr_merge = "\t".join(merges) + "\n"
402             else:
403                 bzr_merge = ""
404
405             svn.wc.prop_set(SVN_PROP_BZR_MERGE, 
406                                  self._get_bzr_merges() + bzr_merge, 
407                                  self.basedir, wc)
408
409             # Set svk:merge
410             svk_merge = ""
411             for merge in merges:
412                 try:
413                     svk_merge += revision_id_to_svk_feature(merge) + "\n"
414                 except InvalidRevisionId:
415                     pass
416
417             svn.wc.prop_set2(SVN_PROP_SVK_MERGE, 
418                              self._get_svk_merges() + svk_merge, self.basedir, 
419                              wc, False)
420         finally:
421             svn.wc.adm_close(wc)
422
423     def add_pending_merge(self, revid):
424         merges = self.pending_merges()
425         merges.append(revid)
426         self.set_pending_merges(existing)
427
428     def pending_merges(self):
429         merged = self._get_bzr_merges().splitlines()
430         wc = self._get_wc()
431         try:
432             merged_data = svn.wc.prop_get(SVN_PROP_BZR_MERGE, self.basedir, wc)
433             if merged_data is None:
434                 set_merged = []
435             else:
436                 set_merged = merged_data.splitlines()
437         finally:
438             svn.wc.adm_close(wc)
439
440         assert (len(merged) == len(set_merged) or 
441                len(merged)+1 == len(set_merged))
442
443         if len(set_merged) > len(merged):
444             return set_merged[-1].split("\t")
445
446         return []
447
448
449 class SvnWorkingTreeFormat(WorkingTreeFormat):
450     def get_format_description(self):
451         return "Subversion Working Copy"
452
453     def initialize(self, a_bzrdir, revision_id=None):
454         # FIXME
455         raise NotImplementedError(self.initialize)
456
457     def open(self, a_bzrdir):
458         # FIXME
459         raise NotImplementedError(self.initialize)
460
461
462 class SvnCheckout(BzrDir):
463     """BzrDir implementation for Subversion checkouts (directories 
464     containing a .svn subdirectory."""
465     def __init__(self, transport, format):
466         super(SvnCheckout, self).__init__(transport, format)
467         self.local_path = transport.local_abspath(".")
468         
469         # Open related remote repository + branch
470         wc = svn.wc.adm_open3(None, self.local_path, False, 0, None)
471         try:
472             svn_url = svn.wc.entry(self.local_path, wc, True).url
473         finally:
474             svn.wc.adm_close(wc)
475
476         self.remote_transport = SvnRaTransport(svn_url)
477         self.svn_root_transport = self.remote_transport.get_root()
478         self.root_transport = self.transport = transport
479
480         self.branch_path = svn_url[len(bzr_to_svn_url(self.svn_root_transport.base)):]
481         self.scheme = BranchingScheme.guess_scheme(self.branch_path)
482         mutter('scheme for %r is %r' % (self.branch_path, self.scheme))
483         if not self.scheme.is_branch(self.branch_path):
484             raise NotBranchError(path=self.transport.base)
485
486     def clone(self, path):
487         raise NotImplementedError(self.clone)
488
489     def open_workingtree(self, _unsupported=False):
490         return SvnWorkingTree(self, self.local_path, self.open_branch())
491
492     def sprout(self, url, revision_id=None, basis=None, force_new_repo=False):
493         # FIXME: honor force_new_repo
494         result = BzrDirFormat.get_default_format().initialize(url)
495         repo = self.open_repository()
496         result_repo = repo.clone(result, revision_id, basis)
497         branch = self.open_branch()
498         branch.sprout(result, revision_id)
499         result.create_workingtree()
500         return result
501
502     def open_repository(self):
503         repos = SvnRepository(self, self.svn_root_transport)
504         return repos
505
506     # Subversion has all-in-one, so a repository is always present
507     find_repository = open_repository
508
509     def create_workingtree(self, revision_id=None):
510         """See BzrDir.create_workingtree().
511
512         Not implemented for Subversion because having a .svn directory
513         implies having a working copy.
514         """
515         raise NotImplementedError(self.create_workingtree)
516
517     def create_branch(self):
518         """See BzrDir.create_branch()."""
519         raise NotImplementedError(self.create_branch)
520
521     def open_branch(self, unsupported=True):
522         """See BzrDir.open_branch()."""
523         repos = self.open_repository()
524
525         try:
526             branch = SvnBranch(self.root_transport.base, repos, self.branch_path)
527         except SubversionException, (msg, num):
528             if num == svn.core.SVN_ERR_WC_NOT_DIRECTORY:
529                raise NotBranchError(path=self.url)
530             raise
531  
532         branch.bzrdir = self
533         return branch
534
535
536 class SvnWorkingTreeDirFormat(BzrDirFormat):
537     """Working Tree implementation that uses Subversion working copies."""
538     _lock_class = TransportLock
539
540     @classmethod
541     def probe_transport(klass, transport):
542         format = klass()
543
544         if isinstance(transport, LocalTransport) and \
545             transport.has(svn.wc.get_adm_dir()):
546             return format
547
548         raise NotBranchError(path=transport.base)
549
550     def _open(self, transport):
551         return SvnCheckout(transport, self)
552
553     def get_format_string(self):
554         return 'Subversion Local Checkout'
555
556     def get_format_description(self):
557         return 'Subversion Local Checkout'
558
559     def initialize_on_transport(self, transport):
560         raise NotImplementedError(self.initialize_on_transport)