/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to workingtree.py

Raise SettingFileIdUnsupported

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2008-2011 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
 
 
18
"""An adapter between a Git index and a Bazaar Working Tree"""
 
19
 
 
20
from __future__ import absolute_import
 
21
 
 
22
from cStringIO import (
 
23
    StringIO,
 
24
    )
 
25
from collections import defaultdict
 
26
import errno
 
27
from dulwich.errors import NotGitRepository
 
28
from dulwich.ignore import (
 
29
    IgnoreFilterManager,
 
30
    )
 
31
from dulwich.index import (
 
32
    Index,
 
33
    changes_from_tree,
 
34
    cleanup_mode,
 
35
    index_entry_from_stat,
 
36
    )
 
37
from dulwich.object_store import (
 
38
    tree_lookup_path,
 
39
    )
 
40
from dulwich.objects import (
 
41
    Blob,
 
42
    S_IFGITLINK,
 
43
    ZERO_SHA,
 
44
    )
 
45
from dulwich.repo import Repo
 
46
import os
 
47
import posixpath
 
48
import re
 
49
import stat
 
50
import sys
 
51
 
 
52
from ... import (
 
53
    errors,
 
54
    conflicts as _mod_conflicts,
 
55
    globbing,
 
56
    ignores,
 
57
    lock,
 
58
    osutils,
 
59
    trace,
 
60
    tree,
 
61
    workingtree,
 
62
    )
 
63
from ...bzr import (
 
64
    inventory,
 
65
    )
 
66
from ...decorators import (
 
67
    needs_read_lock,
 
68
    )
 
69
from ...mutabletree import needs_tree_write_lock
 
70
 
 
71
 
 
72
from .dir import (
 
73
    LocalGitDir,
 
74
    )
 
75
from .tree import (
 
76
    changes_from_git_changes,
 
77
    tree_delta_from_git_changes,
 
78
    )
 
79
from .mapping import (
 
80
    GitFileIdMap,
 
81
    mode_kind,
 
82
    )
 
83
 
 
84
IGNORE_FILENAME = ".gitignore"
 
85
 
 
86
 
 
87
class GitWorkingTree(workingtree.WorkingTree):
 
88
    """A Git working tree."""
 
89
 
 
90
    def __init__(self, controldir, repo, branch, index):
 
91
        self.basedir = controldir.root_transport.local_abspath('.').encode(osutils._fs_enc)
 
92
        self.controldir = controldir
 
93
        self.repository = repo
 
94
        self.store = self.repository._git.object_store
 
95
        self.mapping = self.repository.get_mapping()
 
96
        self._branch = branch
 
97
        self._transport = controldir.transport
 
98
        self._format = GitWorkingTreeFormat()
 
99
        self.index = index
 
100
        self._versioned_dirs = None
 
101
        self.views = self._make_views()
 
102
        self._rules_searcher = None
 
103
        self._detect_case_handling()
 
104
        self._reset_data()
 
105
        self._fileid_map = self._basis_fileid_map.copy()
 
106
        self._lock_mode = None
 
107
        self._lock_count = 0
 
108
 
 
109
    def supports_tree_reference(self):
 
110
        return False
 
111
 
 
112
    def lock_read(self):
 
113
        """Lock the repository for read operations.
 
114
 
 
115
        :return: A breezy.lock.LogicalLockResult.
 
116
        """
 
117
        if not self._lock_mode:
 
118
            self._lock_mode = 'r'
 
119
            self._lock_count = 1
 
120
            self.index.read()
 
121
        else:
 
122
            self._lock_count += 1
 
123
        self.branch.lock_read()
 
124
        return lock.LogicalLockResult(self.unlock)
 
125
 
 
126
    def lock_tree_write(self):
 
127
        if not self._lock_mode:
 
128
            self._lock_mode = 'w'
 
129
            self._lock_count = 1
 
130
            self.index.read()
 
131
        elif self._lock_mode == 'r':
 
132
            raise errors.ReadOnlyError(self)
 
133
        else:
 
134
            self._lock_count +=1
 
135
        self.branch.lock_read()
 
136
        return lock.LogicalLockResult(self.unlock)
 
137
 
 
138
    def lock_write(self, token=None):
 
139
        if not self._lock_mode:
 
140
            self._lock_mode = 'w'
 
141
            self._lock_count = 1
 
142
            self.index.read()
 
143
        elif self._lock_mode == 'r':
 
144
            raise errors.ReadOnlyError(self)
 
145
        else:
 
146
            self._lock_count +=1
 
147
        self.branch.lock_write()
 
148
        return lock.LogicalLockResult(self.unlock)
 
149
 
 
150
    def is_locked(self):
 
151
        return self._lock_count >= 1
 
152
 
 
153
    def get_physical_lock_status(self):
 
154
        return False
 
155
 
 
156
    def unlock(self):
 
157
        if not self._lock_count:
 
158
            return lock.cant_unlock_not_held(self)
 
159
        self.branch.unlock()
 
160
        self._cleanup()
 
161
        self._lock_count -= 1
 
162
        if self._lock_count > 0:
 
163
            return
 
164
        self._lock_mode = None
 
165
 
 
166
    def _cleanup(self):
 
167
        pass
 
168
 
 
169
    def _detect_case_handling(self):
 
170
        try:
 
171
            self._transport.stat(".git/cOnFiG")
 
172
        except errors.NoSuchFile:
 
173
            self.case_sensitive = True
 
174
        else:
 
175
            self.case_sensitive = False
 
176
 
 
177
    def merge_modified(self):
 
178
        return {}
 
179
 
 
180
    def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
 
181
        self.set_parent_ids([p for p, t in parents_list])
 
182
 
 
183
    def iter_children(self, file_id):
 
184
        dpath = self.id2path(file_id) + "/"
 
185
        if dpath in self.index:
 
186
            return
 
187
        for path in self.index:
 
188
            if not path.startswith(dpath):
 
189
                continue
 
190
            if "/" in path[len(dpath):]:
 
191
                # Not a direct child but something further down
 
192
                continue
 
193
            yield self.path2id(path)
 
194
 
 
195
    def _index_add_entry(self, path, kind):
 
196
        assert self._lock_mode is not None
 
197
        assert isinstance(path, basestring)
 
198
        if kind == "directory":
 
199
            # Git indexes don't contain directories
 
200
            return
 
201
        if kind == "file":
 
202
            blob = Blob()
 
203
            try:
 
204
                file, stat_val = self.get_file_with_stat(None, path)
 
205
            except (errors.NoSuchFile, IOError):
 
206
                # TODO: Rather than come up with something here, use the old index
 
207
                file = StringIO()
 
208
                stat_val = os.stat_result(
 
209
                    (stat.S_IFREG | 0644, 0, 0, 0, 0, 0, 0, 0, 0, 0))
 
210
            blob.set_raw_string(file.read())
 
211
        elif kind == "symlink":
 
212
            blob = Blob()
 
213
            try:
 
214
                stat_val = os.lstat(self.abspath(path))
 
215
            except (errors.NoSuchFile, OSError):
 
216
                # TODO: Rather than come up with something here, use the
 
217
                # old index
 
218
                stat_val = os.stat_result(
 
219
                    (stat.S_IFLNK, 0, 0, 0, 0, 0, 0, 0, 0, 0))
 
220
            blob.set_raw_string(
 
221
                self.get_symlink_target(None, path).encode("utf-8"))
 
222
        else:
 
223
            raise AssertionError("unknown kind '%s'" % kind)
 
224
        # Add object to the repository if it didn't exist yet
 
225
        if not blob.id in self.store:
 
226
            self.store.add_object(blob)
 
227
        # Add an entry to the index or update the existing entry
 
228
        flags = 0 # FIXME
 
229
        encoded_path = path.encode("utf-8")
 
230
        self.index[encoded_path] = index_entry_from_stat(
 
231
            stat_val, blob.id, flags)
 
232
        if self._versioned_dirs is not None:
 
233
            self._ensure_versioned_dir(encoded_path)
 
234
 
 
235
    def _ensure_versioned_dir(self, dirname):
 
236
        if dirname in self._versioned_dirs:
 
237
            return
 
238
        if dirname != "":
 
239
            self._ensure_versioned_dir(posixpath.dirname(dirname))
 
240
        self._versioned_dirs.add(dirname)
 
241
 
 
242
    def _load_dirs(self):
 
243
        assert self._lock_mode is not None
 
244
        self._versioned_dirs = set()
 
245
        for p in self.index:
 
246
            self._ensure_versioned_dir(posixpath.dirname(p))
 
247
 
 
248
    def _unversion_path(self, path):
 
249
        assert self._lock_mode is not None
 
250
        encoded_path = path.encode("utf-8")
 
251
        try:
 
252
            del self.index[encoded_path]
 
253
        except KeyError:
 
254
            # A directory, perhaps?
 
255
            for p in list(self.index):
 
256
                if p.startswith(encoded_path+"/"):
 
257
                    del self.index[p]
 
258
        # FIXME: remove empty directories
 
259
 
 
260
    @needs_tree_write_lock
 
261
    def unversion(self, file_ids):
 
262
        for file_id in file_ids:
 
263
            path = self.id2path(file_id)
 
264
            self._unversion_path(path)
 
265
        self.flush()
 
266
 
 
267
    def check_state(self):
 
268
        """Check that the working state is/isn't valid."""
 
269
        pass
 
270
 
 
271
    @needs_tree_write_lock
 
272
    def remove(self, files, verbose=False, to_file=None, keep_files=True,
 
273
        force=False):
 
274
        """Remove nominated files from the working tree metadata.
 
275
 
 
276
        :param files: File paths relative to the basedir.
 
277
        :param keep_files: If true, the files will also be kept.
 
278
        :param force: Delete files and directories, even if they are changed
 
279
            and even if the directories are not empty.
 
280
        """
 
281
        all_files = set() # specified and nested files
 
282
 
 
283
        if isinstance(files, basestring):
 
284
            files = [files]
 
285
 
 
286
        if to_file is None:
 
287
            to_file = sys.stdout
 
288
 
 
289
        files = list(all_files)
 
290
 
 
291
        if len(files) == 0:
 
292
            return # nothing to do
 
293
 
 
294
        # Sort needed to first handle directory content before the directory
 
295
        files.sort(reverse=True)
 
296
 
 
297
        def backup(file_to_backup):
 
298
            abs_path = self.abspath(file_to_backup)
 
299
            backup_name = self.controldir._available_backup_name(file_to_backup)
 
300
            osutils.rename(abs_path, self.abspath(backup_name))
 
301
            return "removed %s (but kept a copy: %s)" % (
 
302
                file_to_backup, backup_name)
 
303
 
 
304
        for f in files:
 
305
            fid = self.path2id(f)
 
306
            if not fid:
 
307
                message = "%s is not versioned." % (f,)
 
308
            else:
 
309
                abs_path = self.abspath(f)
 
310
                if verbose:
 
311
                    # having removed it, it must be either ignored or unknown
 
312
                    if self.is_ignored(f):
 
313
                        new_status = 'I'
 
314
                    else:
 
315
                        new_status = '?'
 
316
                    # XXX: Really should be a more abstract reporter interface
 
317
                    kind_ch = osutils.kind_marker(self.kind(fid))
 
318
                    to_file.write(new_status + '       ' + f + kind_ch + '\n')
 
319
                # Unversion file
 
320
                # FIXME: _unversion_path() is O(size-of-index) for directories
 
321
                self._unversion_path(f)
 
322
                message = "removed %s" % (f,)
 
323
                if osutils.lexists(abs_path):
 
324
                    if (osutils.isdir(abs_path) and
 
325
                        len(os.listdir(abs_path)) > 0):
 
326
                        if force:
 
327
                            osutils.rmtree(abs_path)
 
328
                            message = "deleted %s" % (f,)
 
329
                        else:
 
330
                            message = backup(f)
 
331
                    else:
 
332
                        if not keep_files:
 
333
                            osutils.delete_any(abs_path)
 
334
                            message = "deleted %s" % (f,)
 
335
 
 
336
            # print only one message (if any) per file.
 
337
            if message is not None:
 
338
                trace.note(message)
 
339
        self.flush()
 
340
 
 
341
    def _add(self, files, ids, kinds):
 
342
        for (path, file_id, kind) in zip(files, ids, kinds):
 
343
            if file_id is not None:
 
344
                raise workingtree.SettingFileIdUnsupported()
 
345
            self._index_add_entry(path, kind)
 
346
 
 
347
    @needs_tree_write_lock
 
348
    def smart_add(self, file_list, recurse=True, action=None, save=True):
 
349
        added = []
 
350
        ignored = {}
 
351
        user_dirs = []
 
352
        for filepath in osutils.canonical_relpaths(self.basedir, file_list):
 
353
            abspath = self.abspath(filepath)
 
354
            kind = osutils.file_kind(abspath)
 
355
            if action is not None:
 
356
                file_id = action(self, None, filepath, kind)
 
357
            else:
 
358
                file_id = None
 
359
            if kind in ("file", "symlink"):
 
360
                if save:
 
361
                    self._index_add_entry(filepath, file_id, kind)
 
362
                added.append(filepath)
 
363
            elif kind == "directory":
 
364
                if recurse:
 
365
                    user_dirs.append(filepath)
 
366
            else:
 
367
                raise errors.BadFileKindError(filename=abspath, kind=kind)
 
368
        for user_dir in user_dirs:
 
369
            abs_user_dir = self.abspath(user_dir)
 
370
            for name in os.listdir(abs_user_dir):
 
371
                subp = os.path.join(user_dir, name)
 
372
                if self.is_control_filename(subp) or self.mapping.is_special_file(subp):
 
373
                    continue
 
374
                ignore_glob = self.is_ignored(subp)
 
375
                if ignore_glob is not None:
 
376
                    ignored.setdefault(ignore_glob, []).append(subp)
 
377
                    continue
 
378
                abspath = self.abspath(subp)
 
379
                kind = osutils.file_kind(abspath)
 
380
                if kind == "directory":
 
381
                    user_dirs.append(subp)
 
382
                else:
 
383
                    if action is not None:
 
384
                        file_id = action(self, None, filepath, kind)
 
385
                    else:
 
386
                        file_id = None
 
387
                    if save:
 
388
                        self._index_add_entry(subp, file_id, kind)
 
389
        if added and save:
 
390
            self.flush()
 
391
        return added, ignored
 
392
 
 
393
    def _set_root_id(self, file_id):
 
394
        self._fileid_map.set_file_id("", file_id)
 
395
 
 
396
    @needs_tree_write_lock
 
397
    def move(self, from_paths, to_dir=None, after=False):
 
398
        rename_tuples = []
 
399
        to_abs = self.abspath(to_dir)
 
400
        if not os.path.isdir(to_abs):
 
401
            raise errors.BzrMoveFailedError('', to_dir,
 
402
                errors.NotADirectory(to_abs))
 
403
 
 
404
        for from_rel in from_paths:
 
405
            from_tail = os.path.split(from_rel)[-1]
 
406
            to_rel = os.path.join(to_dir, from_tail)
 
407
            self.rename_one(from_rel, to_rel, after=after)
 
408
            rename_tuples.append((from_rel, to_rel))
 
409
        self.flush()
 
410
        return rename_tuples
 
411
 
 
412
    @needs_tree_write_lock
 
413
    def rename_one(self, from_rel, to_rel, after=False):
 
414
        from_path = from_rel.encode("utf-8")
 
415
        to_path = to_rel.encode("utf-8")
 
416
        if not self.has_filename(to_rel):
 
417
            raise errors.BzrMoveFailedError(from_rel, to_rel,
 
418
                errors.NoSuchFile(to_rel))
 
419
        if not from_path in self.index:
 
420
            raise errors.BzrMoveFailedError(from_rel, to_rel,
 
421
                errors.NotVersionedError(path=from_rel))
 
422
        if not after:
 
423
            os.rename(self.abspath(from_rel), self.abspath(to_rel))
 
424
        self.index[to_path] = self.index[from_path]
 
425
        del self.index[from_path]
 
426
        self.flush()
 
427
 
 
428
    def get_root_id(self):
 
429
        return self.path2id("")
 
430
 
 
431
    def _has_dir(self, path):
 
432
        if path == "":
 
433
            return True
 
434
        if self._versioned_dirs is None:
 
435
            self._load_dirs()
 
436
        return path in self._versioned_dirs
 
437
 
 
438
    @needs_read_lock
 
439
    def path2id(self, path):
 
440
        if type(path) is list:
 
441
            path = u"/".join(path)
 
442
        encoded_path = path.encode("utf-8")
 
443
        if self._is_versioned(encoded_path):
 
444
            return self._fileid_map.lookup_file_id(encoded_path)
 
445
        return None
 
446
 
 
447
    def _iter_files_recursive(self, from_dir=None):
 
448
        if from_dir is None:
 
449
            from_dir = ""
 
450
        for (dirpath, dirnames, filenames) in os.walk(self.abspath(from_dir)):
 
451
            dir_relpath = dirpath[len(self.basedir):].strip("/")
 
452
            if self.controldir.is_control_filename(dir_relpath):
 
453
                continue
 
454
            for filename in filenames:
 
455
                if not self.mapping.is_special_file(filename):
 
456
                    yield os.path.join(dir_relpath, filename)
 
457
 
 
458
    @needs_read_lock
 
459
    def extras(self):
 
460
        """Yield all unversioned files in this WorkingTree.
 
461
        """
 
462
        return set(self._iter_files_recursive()) - set(self.index)
 
463
 
 
464
    @needs_tree_write_lock
 
465
    def flush(self):
 
466
        # TODO: Maybe this should only write on dirty ?
 
467
        if self._lock_mode != 'w':
 
468
            raise errors.NotWriteLocked(self)
 
469
        self.index.write()
 
470
 
 
471
    @needs_read_lock
 
472
    def __iter__(self):
 
473
        for path in self.index:
 
474
            yield self.path2id(path)
 
475
        self._load_dirs()
 
476
        for path in self._versioned_dirs:
 
477
            yield self.path2id(path)
 
478
 
 
479
    def has_or_had_id(self, file_id):
 
480
        if self.has_id(file_id):
 
481
            return True
 
482
        if self.had_id(file_id):
 
483
            return True
 
484
        return False
 
485
 
 
486
    def had_id(self, file_id):
 
487
        path = self._basis_fileid_map.lookup_file_id(file_id)
 
488
        try:
 
489
            head = self.repository._git.head()
 
490
        except KeyError:
 
491
            # Assume no if basis is not accessible
 
492
            return False
 
493
        if head == ZERO_SHA:
 
494
            return False
 
495
        root_tree = self.store[head].tree
 
496
        try:
 
497
            tree_lookup_path(self.store.__getitem__, root_tree, path)
 
498
        except KeyError:
 
499
            return False
 
500
        else:
 
501
            return True
 
502
 
 
503
    def has_id(self, file_id):
 
504
        try:
 
505
            self.id2path(file_id)
 
506
        except errors.NoSuchId:
 
507
            return False
 
508
        else:
 
509
            return True
 
510
 
 
511
    @needs_read_lock
 
512
    def id2path(self, file_id):
 
513
        assert type(file_id) is str, "file id not a string: %r" % file_id
 
514
        file_id = osutils.safe_utf8(file_id)
 
515
        path = self._fileid_map.lookup_path(file_id)
 
516
        # FIXME: What about directories?
 
517
        if self._is_versioned(path):
 
518
            return path.decode("utf-8")
 
519
        raise errors.NoSuchId(self, file_id)
 
520
 
 
521
    def get_file_mtime(self, file_id, path=None):
 
522
        """See Tree.get_file_mtime."""
 
523
        if not path:
 
524
            path = self.id2path(file_id)
 
525
        return os.lstat(self.abspath(path)).st_mtime
 
526
 
 
527
    def is_ignored(self, filename):
 
528
        r"""Check whether the filename matches an ignore pattern.
 
529
 
 
530
        If the file is ignored, returns the pattern which caused it to
 
531
        be ignored, otherwise None.  So this can simply be used as a
 
532
        boolean if desired."""
 
533
        if getattr(self, '_global_ignoreglobster', None) is None:
 
534
            ignore_globs = set()
 
535
            ignore_globs.update(ignores.get_runtime_ignores())
 
536
            ignore_globs.update(ignores.get_user_ignores())
 
537
            self._global_ignoreglobster = globbing.ExceptionGlobster(ignore_globs)
 
538
        match = self._global_ignoreglobster.match(filename)
 
539
        if match is not None:
 
540
            return match
 
541
        if osutils.file_kind(self.abspath(filename)) == 'directory':
 
542
            filename += b'/'
 
543
        ignore_manager = self._get_ignore_manager()
 
544
        ps = list(ignore_manager.find_matching(filename))
 
545
        if not ps:
 
546
            return None
 
547
        if not ps[-1].is_exclude:
 
548
            return None
 
549
        return bytes(ps[-1])
 
550
 
 
551
    def _get_ignore_manager(self):
 
552
        ignoremanager = getattr(self, '_ignoremanager', None)
 
553
        if ignoremanager is not None:
 
554
            return ignoremanager
 
555
 
 
556
        ignore_manager = IgnoreFilterManager.from_repo(self.repository._git)
 
557
        self._ignoremanager = ignore_manager
 
558
        return ignore_manager
 
559
 
 
560
    def set_last_revision(self, revid):
 
561
        self._change_last_revision(revid)
 
562
 
 
563
    def _reset_data(self):
 
564
        try:
 
565
            head = self.repository._git.head()
 
566
        except KeyError, name:
 
567
            raise errors.NotBranchError("branch %s at %s" % (name,
 
568
                self.repository.base))
 
569
        if head == ZERO_SHA:
 
570
            self._basis_fileid_map = GitFileIdMap({}, self.mapping)
 
571
        else:
 
572
            self._basis_fileid_map = self.mapping.get_fileid_map(
 
573
                self.store.__getitem__, self.store[head].tree)
 
574
 
 
575
    @needs_read_lock
 
576
    def get_file_verifier(self, file_id, path=None, stat_value=None):
 
577
        if path is None:
 
578
            path = self.id2path(file_id)
 
579
        try:
 
580
            return ("GIT", self.index[path][-2])
 
581
        except KeyError:
 
582
            if self._has_dir(path):
 
583
                return ("GIT", None)
 
584
            raise errors.NoSuchId(self, file_id)
 
585
 
 
586
    @needs_read_lock
 
587
    def get_file_sha1(self, file_id, path=None, stat_value=None):
 
588
        if not path:
 
589
            path = self.id2path(file_id)
 
590
        abspath = self.abspath(path).encode(osutils._fs_enc)
 
591
        try:
 
592
            return osutils.sha_file_by_name(abspath)
 
593
        except OSError, (num, msg):
 
594
            if num in (errno.EISDIR, errno.ENOENT):
 
595
                return None
 
596
            raise
 
597
 
 
598
    def revision_tree(self, revid):
 
599
        return self.repository.revision_tree(revid)
 
600
 
 
601
    def _is_versioned(self, path):
 
602
        assert self._lock_mode is not None
 
603
        return (path in self.index or self._has_dir(path))
 
604
 
 
605
    def filter_unversioned_files(self, files):
 
606
        return set([p for p in files if not self._is_versioned(p.encode("utf-8"))])
 
607
 
 
608
    def _get_dir_ie(self, path, parent_id):
 
609
        file_id = self.path2id(path)
 
610
        return inventory.InventoryDirectory(file_id,
 
611
            posixpath.basename(path).strip("/"), parent_id)
 
612
 
 
613
    def _add_missing_parent_ids(self, path, dir_ids):
 
614
        if path in dir_ids:
 
615
            return []
 
616
        parent = posixpath.dirname(path).strip("/")
 
617
        ret = self._add_missing_parent_ids(parent, dir_ids)
 
618
        parent_id = dir_ids[parent]
 
619
        ie = self._get_dir_ie(path, parent_id)
 
620
        dir_ids[path] = ie.file_id
 
621
        ret.append((path, ie))
 
622
        return ret
 
623
 
 
624
    def _get_file_ie(self, name, path, value, parent_id):
 
625
        assert isinstance(name, unicode)
 
626
        assert isinstance(path, unicode)
 
627
        assert isinstance(value, tuple) and len(value) == 10
 
628
        (ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags) = value
 
629
        file_id = self.path2id(path)
 
630
        if type(file_id) != str:
 
631
            raise AssertionError
 
632
        kind = mode_kind(mode)
 
633
        ie = inventory.entry_factory[kind](file_id, name, parent_id)
 
634
        if kind == 'symlink':
 
635
            ie.symlink_target = self.get_symlink_target(file_id)
 
636
        else:
 
637
            data = self.get_file_text(file_id, path)
 
638
            ie.text_sha1 = osutils.sha_string(data)
 
639
            ie.text_size = len(data)
 
640
            ie.executable = self.is_executable(file_id, path)
 
641
        ie.revision = None
 
642
        return ie
 
643
 
 
644
    def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
 
645
        mode = stat_result.st_mode
 
646
        return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
 
647
 
 
648
    @needs_read_lock
 
649
    def stored_kind(self, file_id, path=None):
 
650
        if path is None:
 
651
            path = self.id2path(file_id)
 
652
        try:
 
653
            return mode_kind(self.index[path.encode("utf-8")][4])
 
654
        except KeyError:
 
655
            # Maybe it's a directory?
 
656
            if self._has_dir(path):
 
657
                return "directory"
 
658
            raise errors.NoSuchId(self, file_id)
 
659
 
 
660
    def is_executable(self, file_id, path=None):
 
661
        if getattr(self, "_supports_executable", osutils.supports_executable)():
 
662
            if not path:
 
663
                path = self.id2path(file_id)
 
664
            mode = os.lstat(self.abspath(path)).st_mode
 
665
            return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
 
666
        else:
 
667
            basis_tree = self.basis_tree()
 
668
            if file_id in basis_tree:
 
669
                return basis_tree.is_executable(file_id)
 
670
            # Default to not executable
 
671
            return False
 
672
 
 
673
    def _is_executable_from_path_and_stat(self, path, stat_result):
 
674
        if getattr(self, "_supports_executable", osutils.supports_executable)():
 
675
            return self._is_executable_from_path_and_stat_from_stat(path, stat_result)
 
676
        else:
 
677
            return self._is_executable_from_path_and_stat_from_basis(path, stat_result)
 
678
 
 
679
    @needs_read_lock
 
680
    def list_files(self, include_root=False, from_dir=None, recursive=True):
 
681
        if from_dir is None:
 
682
            from_dir = ""
 
683
        dir_ids = {}
 
684
        fk_entries = {'directory': workingtree.TreeDirectory,
 
685
                      'file': workingtree.TreeFile,
 
686
                      'symlink': workingtree.TreeLink}
 
687
        root_ie = self._get_dir_ie(u"", None)
 
688
        if include_root and not from_dir:
 
689
            yield "", "V", root_ie.kind, root_ie.file_id, root_ie
 
690
        dir_ids[u""] = root_ie.file_id
 
691
        if recursive:
 
692
            path_iterator = self._iter_files_recursive(from_dir)
 
693
        else:
 
694
            if from_dir is None:
 
695
                start = self.basedir
 
696
            else:
 
697
                start = os.path.join(self.basedir, from_dir)
 
698
            path_iterator = sorted([os.path.join(from_dir, name) for name in
 
699
                os.listdir(start) if not self.controldir.is_control_filename(name)
 
700
                and not self.mapping.is_special_file(name)])
 
701
        for path in path_iterator:
 
702
            try:
 
703
                value = self.index[path]
 
704
            except KeyError:
 
705
                value = None
 
706
            path = path.decode("utf-8")
 
707
            parent, name = posixpath.split(path)
 
708
            for dir_path, dir_ie in self._add_missing_parent_ids(parent, dir_ids):
 
709
                yield dir_path, "V", dir_ie.kind, dir_ie.file_id, dir_ie
 
710
            if value is not None:
 
711
                ie = self._get_file_ie(name, path, value, dir_ids[parent])
 
712
                yield path, "V", ie.kind, ie.file_id, ie
 
713
            else:
 
714
                kind = osutils.file_kind(self.abspath(path))
 
715
                ie = fk_entries[kind]()
 
716
                yield path, ("I" if self.is_ignored(path) else "?"), kind, None, ie
 
717
 
 
718
    @needs_read_lock
 
719
    def all_file_ids(self):
 
720
        ids = {u"": self.path2id("")}
 
721
        for path in self.index:
 
722
            if self.mapping.is_special_file(path):
 
723
                continue
 
724
            path = path.decode("utf-8")
 
725
            parent = posixpath.dirname(path).strip("/")
 
726
            for e in self._add_missing_parent_ids(parent, ids):
 
727
                pass
 
728
            ids[path] = self.path2id(path)
 
729
        return set(ids.values())
 
730
 
 
731
    def _directory_is_tree_reference(self, path):
 
732
        # FIXME: Check .gitsubmodules for path
 
733
        return False
 
734
 
 
735
    @needs_read_lock
 
736
    def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
 
737
        # FIXME: Is return order correct?
 
738
        if yield_parents:
 
739
            raise NotImplementedError(self.iter_entries_by_dir)
 
740
        if specific_file_ids is not None:
 
741
            specific_paths = [self.id2path(file_id) for file_id in specific_file_ids]
 
742
            if specific_paths in ([u""], []):
 
743
                specific_paths = None
 
744
            else:
 
745
                specific_paths = set(specific_paths)
 
746
        else:
 
747
            specific_paths = None
 
748
        root_ie = self._get_dir_ie(u"", None)
 
749
        if specific_paths is None:
 
750
            yield u"", root_ie
 
751
        dir_ids = {u"": root_ie.file_id}
 
752
        for path, value in self.index.iteritems():
 
753
            if self.mapping.is_special_file(path):
 
754
                continue
 
755
            path = path.decode("utf-8")
 
756
            if specific_paths is not None and not path in specific_paths:
 
757
                continue
 
758
            (parent, name) = posixpath.split(path)
 
759
            try:
 
760
                file_ie = self._get_file_ie(name, path, value, None)
 
761
            except IOError:
 
762
                continue
 
763
            for (dir_path, dir_ie) in self._add_missing_parent_ids(parent,
 
764
                    dir_ids):
 
765
                yield dir_path, dir_ie
 
766
            file_ie.parent_id = self.path2id(parent)
 
767
            yield path, file_ie
 
768
 
 
769
    @needs_read_lock
 
770
    def conflicts(self):
 
771
        # FIXME:
 
772
        return _mod_conflicts.ConflictList()
 
773
 
 
774
    def update_basis_by_delta(self, new_revid, delta):
 
775
        # The index just contains content, which won't have changed.
 
776
        self._reset_data()
 
777
 
 
778
    @needs_read_lock
 
779
    def get_canonical_inventory_path(self, path):
 
780
        for p in self.index:
 
781
            if p.lower() == path.lower():
 
782
                return p
 
783
        else:
 
784
            return path
 
785
 
 
786
    @needs_read_lock
 
787
    def _walkdirs(self, prefix=""):
 
788
        if prefix != "":
 
789
            prefix += "/"
 
790
        per_dir = defaultdict(list)
 
791
        for path, value in self.index.iteritems():
 
792
            if self.mapping.is_special_file(path):
 
793
                continue
 
794
            if not path.startswith(prefix):
 
795
                continue
 
796
            (dirname, child_name) = posixpath.split(path)
 
797
            dirname = dirname.decode("utf-8")
 
798
            dir_file_id = self.path2id(dirname)
 
799
            assert isinstance(value, tuple) and len(value) == 10
 
800
            (ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags) = value
 
801
            stat_result = os.stat_result((mode, ino,
 
802
                    dev, 1, uid, gid, size,
 
803
                    0, mtime, ctime))
 
804
            per_dir[(dirname, dir_file_id)].append(
 
805
                (path.decode("utf-8"), child_name.decode("utf-8"),
 
806
                mode_kind(mode), stat_result,
 
807
                self.path2id(path.decode("utf-8")),
 
808
                mode_kind(mode)))
 
809
        return per_dir.iteritems()
 
810
 
 
811
    def _lookup_entry(self, path, update_index=False):
 
812
        assert type(path) == str
 
813
        entry = self.index[path]
 
814
        index_mode = entry[-6]
 
815
        index_sha = entry[-2]
 
816
        disk_path = os.path.join(self.basedir, path)
 
817
        try:
 
818
            disk_stat = os.lstat(disk_path)
 
819
        except OSError, (num, msg):
 
820
            if num in (errno.EISDIR, errno.ENOENT):
 
821
                raise KeyError(path)
 
822
            raise
 
823
        disk_mtime = disk_stat.st_mtime
 
824
        if isinstance(entry[1], tuple):
 
825
            index_mtime = entry[1][0]
 
826
        else:
 
827
            index_mtime = int(entry[1])
 
828
        mtime_delta = (disk_mtime - index_mtime)
 
829
        disk_mode = cleanup_mode(disk_stat.st_mode)
 
830
        if mtime_delta > 0 or disk_mode != index_mode:
 
831
            if stat.S_ISDIR(disk_mode):
 
832
                try:
 
833
                    subrepo = Repo(disk_path)
 
834
                except NotGitRepository:
 
835
                    return (None, None)
 
836
                else:
 
837
                    disk_mode = S_IFGITLINK
 
838
                    git_id = subrepo.head()
 
839
            elif stat.S_ISLNK(disk_mode):
 
840
                blob = Blob.from_string(os.readlink(disk_path).encode('utf-8'))
 
841
                git_id = blob.id
 
842
            elif stat.S_ISREG(disk_mode):
 
843
                with open(disk_path, 'r') as f:
 
844
                    blob = Blob.from_string(f.read())
 
845
                git_id = blob.id
 
846
            else:
 
847
                raise AssertionError
 
848
            if update_index:
 
849
                flags = 0 # FIXME
 
850
                self.index[path] = index_entry_from_stat(disk_stat, git_id, flags, disk_mode)
 
851
            return (git_id, disk_mode)
 
852
        return (index_sha, index_mode)
 
853
 
 
854
 
 
855
class GitWorkingTreeFormat(workingtree.WorkingTreeFormat):
 
856
 
 
857
    _tree_class = GitWorkingTree
 
858
 
 
859
    supports_versioned_directories = False
 
860
 
 
861
    supports_setting_file_ids = False
 
862
 
 
863
    @property
 
864
    def _matchingbzrdir(self):
 
865
        from .dir import LocalGitControlDirFormat
 
866
        return LocalGitControlDirFormat()
 
867
 
 
868
    def get_format_description(self):
 
869
        return "Git Working Tree"
 
870
 
 
871
    def initialize(self, a_controldir, revision_id=None, from_branch=None,
 
872
                   accelerator_tree=None, hardlink=False):
 
873
        """See WorkingTreeFormat.initialize()."""
 
874
        if not isinstance(a_controldir, LocalGitDir):
 
875
            raise errors.IncompatibleFormat(self, a_controldir)
 
876
        index = Index(a_controldir.root_transport.local_abspath(".git/index"))
 
877
        index.write()
 
878
        return GitWorkingTree(a_controldir, a_controldir.open_repository(),
 
879
            a_controldir.open_branch(), index)
 
880
 
 
881
 
 
882
class InterIndexGitTree(tree.InterTree):
 
883
    """InterTree that works between a Git revision tree and an index."""
 
884
 
 
885
    def __init__(self, source, target):
 
886
        super(InterIndexGitTree, self).__init__(source, target)
 
887
        self._index = target.index
 
888
 
 
889
    @classmethod
 
890
    def is_compatible(cls, source, target):
 
891
        from .repository import GitRevisionTree
 
892
        return (isinstance(source, GitRevisionTree) and
 
893
                isinstance(target, GitWorkingTree))
 
894
 
 
895
    @needs_read_lock
 
896
    def compare(self, want_unchanged=False, specific_files=None,
 
897
                extra_trees=None, require_versioned=False, include_root=False,
 
898
                want_unversioned=False):
 
899
        # FIXME: Handle include_root
 
900
        changes = changes_between_git_tree_and_index(
 
901
            self.source.store, self.source.tree,
 
902
            self.target, want_unchanged=want_unchanged,
 
903
            want_unversioned=want_unversioned)
 
904
        source_fileid_map = self.source._fileid_map
 
905
        target_fileid_map = self.target._fileid_map
 
906
        ret = tree_delta_from_git_changes(changes, self.target.mapping,
 
907
            (source_fileid_map, target_fileid_map),
 
908
            specific_file=specific_files, require_versioned=require_versioned)
 
909
        if want_unversioned:
 
910
            for e in self.target.extras():
 
911
                ret.unversioned.append((e, None,
 
912
                    osutils.file_kind(self.target.abspath(e))))
 
913
        return ret
 
914
 
 
915
    @needs_read_lock
 
916
    def iter_changes(self, include_unchanged=False, specific_files=None,
 
917
        pb=None, extra_trees=[], require_versioned=True,
 
918
        want_unversioned=False):
 
919
        changes = changes_between_git_tree_and_index(
 
920
            self.source.store, self.source.tree,
 
921
            self.target, want_unchanged=include_unchanged,
 
922
            want_unversioned=want_unversioned)
 
923
        return changes_from_git_changes(changes, self.target.mapping,
 
924
            specific_file=specific_files)
 
925
 
 
926
 
 
927
tree.InterTree.register_optimiser(InterIndexGitTree)
 
928
 
 
929
 
 
930
def changes_between_git_tree_and_index(object_store, tree, target,
 
931
        want_unchanged=False, want_unversioned=False, update_index=False):
 
932
    """Determine the changes between a git tree and a working tree with index.
 
933
 
 
934
    """
 
935
 
 
936
    names = target.index._byname.keys()
 
937
    for (name, mode, sha) in changes_from_tree(names, target._lookup_entry,
 
938
            object_store, tree, want_unchanged=want_unchanged):
 
939
        if name == (None, None):
 
940
            continue
 
941
        yield (name, mode, sha)