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