Merge trunk changes.
[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         mutter('working inv: %r' % self.read_working_inventory().entries())
57
58         self.base_revid = branch.repository.generate_revision_id(
59                     self.base_revnum, branch.branch_path)
60         mutter('basis inv: %r' % self.basis_tree().inventory.entries())
61         self.controldir = os.path.join(self.basedir, svn.wc.get_adm_dir(), 'bzr')
62         try:
63             os.makedirs(self.controldir)
64             os.makedirs(os.path.join(self.controldir, 'lock'))
65         except OSError:
66             pass
67         control_transport = bzrdir.transport.clone(os.path.join(svn.wc.get_adm_dir(), 'bzr'))
68         self._control_files = LockableFiles(control_transport, 'lock', LockDir)
69
70     def lock_write(self):
71         pass
72
73     def lock_read(self):
74         pass
75
76     def unlock(self):
77         pass
78
79     def get_ignore_list(self):
80         ignores = []
81
82         def dir_add(wc, prefix):
83             ignores.append(os.path.join(prefix, svn.wc.get_adm_dir()))
84             for pat in svn.wc.get_ignores(svn_config, wc):
85                 ignores.append(os.path.join(prefix, pat))
86
87             entries = svn.wc.entries_read(wc, False)
88             for entry in entries:
89                 if entry == "":
90                     continue
91
92                 if entries[entry].kind != svn.core.svn_node_dir:
93                     continue
94
95                 subprefix = os.path.join(prefix, entry)
96
97                 subwc = svn.wc.adm_open3(wc, self.abspath(subprefix), False, 0, None)
98                 try:
99                     dir_add(subwc, subprefix)
100                 finally:
101                     svn.wc.adm_close(subwc)
102
103         wc = self._get_wc()
104         try:
105             dir_add(wc, "")
106         finally:
107             svn.wc.adm_close(wc)
108
109         return ignores
110
111     def _write_inventory(self, inv):
112         pass
113
114     def is_ignored(self, filename):
115         if svn.wc.is_adm_dir(os.path.basename(filename)):
116             return True
117
118         (wc, name) = self._get_rel_wc(filename)
119         assert wc
120         try:
121             ignores = svn.wc.get_ignores(svn_config, wc)
122             from fnmatch import fnmatch
123             for pattern in ignores:
124                 if fnmatch(name, pattern):
125                     return True
126             return False
127         finally:
128             svn.wc.adm_close(wc)
129
130     def is_control_filename(self, path):
131         return svn.wc.is_adm_dir(path)
132
133     def remove(self, files, verbose=False, to_file=None):
134         wc = self._get_wc(write_lock=True)
135         try:
136             for file in files:
137                 svn.wc.delete2(self.abspath(file), wc, None, None, None)
138         finally:
139             svn.wc.adm_close(wc)
140
141     def _get_wc(self, relpath="", write_lock=False):
142         return svn.wc.adm_open3(None, self.abspath(relpath).rstrip("/"), write_lock, 0, None)
143
144     def _get_rel_wc(self, relpath, write_lock=False):
145         dir = os.path.dirname(relpath)
146         file = os.path.basename(relpath)
147         return (self._get_wc(dir, write_lock), file)
148
149     def move(self, from_paths, to_name):
150         revt = svn.core.svn_opt_revision_t()
151         revt.kind = svn.core.svn_opt_revision_working
152         to_wc = self._get_wc(to_name, write_lock=True)
153         try:
154             for entry in from_paths:
155                 svn.wc.copy(self.abspath(entry), to_wc, os.path.basename(entry), None, None)
156         finally:
157             svn.wc.adm_close(to_wc)
158
159         for entry in from_paths:
160             self.remove([entry])
161
162     def rename_one(self, from_rel, to_rel):
163         revt = svn.core.svn_opt_revision_t()
164         revt.kind = svn.core.svn_opt_revision_unspecified
165         (to_wc, to_file) = self._get_rel_wc(to_rel, write_lock=True)
166         try:
167             svn.wc.copy(self.abspath(from_rel), to_wc, to_file, None, None)
168             svn.wc.delete2(self.abspath(from_rel), to_wc, None, None, None)
169         finally:
170             svn.wc.adm_close(to_wc)
171
172     def read_working_inventory(self):
173         inv = Inventory()
174
175         def add_file_to_inv(relpath, id, revid, parent_id):
176             """Add a file to the inventory."""
177             file = InventoryFile(id, os.path.basename(relpath), parent_id)
178             file.revision = revid
179             try:
180                 data = fingerprint_file(open(self.abspath(relpath)))
181                 file.text_sha1 = data['sha1']
182                 file.text_size = data['size']
183                 file.executable = self.is_executable(id, relpath)
184                 inv.add(file)
185             except IOError:
186                 # Ignore non-existing files
187                 pass
188
189         def find_copies(url, relpath=""):
190             wc = self._get_wc(relpath)
191             entries = svn.wc.entries_read(wc, False)
192             for entry in entries.values():
193                 subrelpath = os.path.join(relpath, entry.name)
194                 if entry.name == "" or entry.kind != 'directory':
195                     if ((entry.copyfrom_url == url or entry.url == url) and 
196                         not (entry.schedule in (svn.wc.schedule_delete,
197                                                 svn.wc.schedule_replace))):
198                         yield os.path.join(
199                                 self.branch.branch_path.strip("/"), 
200                                 subrelpath)
201                 else:
202                     find_copies(subrelpath)
203             svn.wc.adm_close(wc)
204
205         def find_ids(entry):
206             relpath = entry.url[len(entry.repos):].strip("/")
207             if entry.schedule == svn.wc.schedule_normal:
208                 assert entry.revision >= 0
209                 # Keep old id
210                 mutter('stay: %r' % relpath)
211                 return self.branch.repository.path_to_file_id(entry.revision, 
212                         relpath)
213             elif entry.schedule == svn.wc.schedule_delete:
214                 return (None, None)
215             elif (entry.schedule == svn.wc.schedule_add or 
216                   entry.schedule == svn.wc.schedule_replace):
217                 # See if the file this file was copied from disappeared
218                 # and has no other copies -> in that case, take id of other file
219                 mutter('copies(%r): %r' % (relpath, list(find_copies(entry.copyfrom_url))))
220                 if entry.copyfrom_url and list(find_copies(entry.copyfrom_url)) == [relpath]:
221                     return self.branch.repository.path_to_file_id(entry.copyfrom_rev,
222                         entry.copyfrom_url[len(entry.repos):])
223                 return ("NEW-" + escape_svn_path(entry.url[len(entry.repos):].strip("/")), None)
224             else:
225                 assert 0
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(commit_info.revision, self.branch.branch_path)
337
338         self.base_revid = revid
339         self.branch._revision_history.append(revid)
340
341         return revid
342
343     def add(self, files, ids=None):
344         assert isinstance(files, list)
345         wc = self._get_wc(write_lock=True)
346         try:
347             for f in files:
348                 try:
349                     svn.wc.add2(os.path.join(self.basedir, f), wc, None, 0, 
350                             None, None, None)
351                     if ids:
352                         svn.wc.prop_set2('bzr:fileid', ids.pop(), relpath, wc, 
353                                 False)
354                 except SubversionException, (_, num):
355                     if num == svn.core.SVN_ERR_ENTRY_EXISTS:
356                         continue
357                     elif num == svn.core.SVN_ERR_WC_PATH_NOT_FOUND:
358                         raise NoSuchFile(path=f)
359                     raise
360         finally:
361             svn.wc.adm_close(wc)
362
363     def basis_tree(self):
364         if self.base_revid is None or self.base_revid == NULL_REVISION:
365             return self.branch.repository.revision_tree(self.base_revid)
366
367         return SvnBasisTree(self, self.base_revid)
368
369     def pull(self, source, overwrite=False, stop_revision=None):
370         if stop_revision is None:
371             stop_revision = self.branch.last_revision()
372         rev = svn.core.svn_opt_revision_t()
373         rev.kind = svn.core.svn_opt_revision_number
374         rev.value.number = self.branch.repository.parse_revision_id(stop_revision)[1]
375         fetched = svn.client.update(self.basedir, rev, True, self.client_ctx)
376         self.base_revid = self.branch.repository.generate_revision_id(fetched, self.branch.branch_path)
377         return fetched-rev.value.number
378
379     def get_file_sha1(self, file_id, path=None, stat_value=None):
380         if not path:
381             path = self._inventory.id2path(file_id)
382
383         return fingerprint_file(open(self.abspath(path)))['sha1']
384
385     def _get_bzr_merges(self):
386         return self.branch.repository._get_branch_prop(self.branch.branch_path, 
387                                             self.base_revnum, 
388                                             SVN_PROP_BZR_MERGE, "")
389
390     def _get_svk_merges(self):
391         return self.branch.repository._get_branch_prop(self.branch.branch_path, 
392                                             self.base_revnum, 
393                                             SVN_PROP_SVK_MERGE, "")
394
395     def set_pending_merges(self, merges):
396         wc = self._get_wc(write_lock=True)
397         try:
398             # Set bzr:merge
399             if len(merges) > 0:
400                 bzr_merge = "\t".join(merges) + "\n"
401             else:
402                 bzr_merge = ""
403
404             svn.wc.prop_set(SVN_PROP_BZR_MERGE, 
405                                  self._get_bzr_merges() + bzr_merge, 
406                                  self.basedir, wc)
407
408             # Set svk:merge
409             svk_merge = ""
410             for merge in merges:
411                 try:
412                     svk_merge += revision_id_to_svk_feature(merge) + "\n"
413                 except InvalidRevisionId:
414                     pass
415
416             svn.wc.prop_set2(SVN_PROP_SVK_MERGE, 
417                              self._get_svk_merges() + svk_merge, self.basedir, 
418                              wc, False)
419         finally:
420             svn.wc.adm_close(wc)
421
422     def add_pending_merge(self, revid):
423         merges = self.pending_merges()
424         merges.append(revid)
425         self.set_pending_merges(existing)
426
427     def pending_merges(self):
428         merged = self._get_bzr_merges().splitlines()
429         wc = self._get_wc()
430         try:
431             merged_data = svn.wc.prop_get(SVN_PROP_BZR_MERGE, self.basedir, wc)
432             if merged_data is None:
433                 set_merged = []
434             else:
435                 set_merged = merged_data.splitlines()
436         finally:
437             svn.wc.adm_close(wc)
438
439         assert (len(merged) == len(set_merged) or 
440                len(merged)+1 == len(set_merged))
441
442         if len(set_merged) > len(merged):
443             return set_merged[-1].split("\t")
444
445         return []
446
447
448 class SvnWorkingTreeFormat(WorkingTreeFormat):
449     def get_format_description(self):
450         return "Subversion Working Copy"
451
452     def initialize(self, a_bzrdir, revision_id=None):
453         # FIXME
454         raise NotImplementedError(self.initialize)
455
456     def open(self, a_bzrdir):
457         # FIXME
458         raise NotImplementedError(self.initialize)
459
460
461 class SvnCheckout(BzrDir):
462     """BzrDir implementation for Subversion checkouts (directories 
463     containing a .svn subdirectory."""
464     def __init__(self, transport, format):
465         super(SvnCheckout, self).__init__(transport, format)
466         self.local_path = transport.local_abspath(".")
467         
468         # Open related remote repository + branch
469         wc = svn.wc.adm_open3(None, self.local_path, False, 0, None)
470         try:
471             svn_url = svn.wc.entry(self.local_path, wc, True).url
472         finally:
473             svn.wc.adm_close(wc)
474
475         self.remote_transport = SvnRaTransport(svn_url)
476         self.svn_root_transport = self.remote_transport.get_root()
477         self.root_transport = self.transport = transport
478
479         self.branch_path = svn_url[len(bzr_to_svn_url(self.svn_root_transport.base)):]
480         self.scheme = BranchingScheme.guess_scheme(self.branch_path)
481         mutter('scheme for %r is %r' % (self.branch_path, self.scheme))
482         if not self.scheme.is_branch(self.branch_path):
483             raise NotBranchError(path=self.transport.base)
484
485     def clone(self, path):
486         raise NotImplementedError(self.clone)
487
488     def open_workingtree(self, _unsupported=False):
489         return SvnWorkingTree(self, self.local_path, self.open_branch())
490
491     def sprout(self, url, revision_id=None, basis=None, force_new_repo=False):
492         # FIXME: honor force_new_repo
493         result = BzrDirFormat.get_default_format().initialize(url)
494         repo = self.open_repository()
495         result_repo = repo.clone(result, revision_id, basis)
496         branch = self.open_branch()
497         branch.sprout(result, revision_id)
498         result.create_workingtree()
499         return result
500
501     def open_repository(self):
502         repos = SvnRepository(self, self.svn_root_transport)
503         return repos
504
505     # Subversion has all-in-one, so a repository is always present
506     find_repository = open_repository
507
508     def create_workingtree(self, revision_id=None):
509         """See BzrDir.create_workingtree().
510
511         Not implemented for Subversion because having a .svn directory
512         implies having a working copy.
513         """
514         raise NotImplementedError(self.create_workingtree)
515
516     def create_branch(self):
517         """See BzrDir.create_branch()."""
518         raise NotImplementedError(self.create_branch)
519
520     def open_branch(self, unsupported=True):
521         """See BzrDir.open_branch()."""
522         repos = self.open_repository()
523
524         try:
525             branch = SvnBranch(self.root_transport.base, repos, self.branch_path)
526         except SubversionException, (msg, num):
527             if num == svn.core.SVN_ERR_WC_NOT_DIRECTORY:
528                raise NotBranchError(path=self.url)
529             raise
530  
531         branch.bzrdir = self
532         return branch
533
534
535 class SvnWorkingTreeDirFormat(BzrDirFormat):
536     """Working Tree implementation that uses Subversion working copies."""
537     _lock_class = TransportLock
538
539     @classmethod
540     def probe_transport(klass, transport):
541         format = klass()
542
543         if isinstance(transport, LocalTransport) and \
544             transport.has(svn.wc.get_adm_dir()):
545             return format
546
547         raise NotBranchError(path=transport.base)
548
549     def _open(self, transport):
550         return SvnCheckout(transport, self)
551
552     def get_format_string(self):
553         return 'Subversion Local Checkout'
554
555     def get_format_description(self):
556         return 'Subversion Local Checkout'
557
558     def initialize_on_transport(self, transport):
559         raise NotImplementedError(self.initialize_on_transport)