/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

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-04-13 23:16:57 UTC
  • mfrom: (1662.1.1 bzr.mbp.integration)
  • Revision ID: pqm@pqm.ubuntu.com-20060413231657-bce3d67d3e7a4f2b
(mbp/olaf) push/pull/merge --remember improvements

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)