summaryrefslogtreecommitdiff
path: root/libchrome_tools/uprev/lazytree.py
blob: d76fa5c8c19bf3a7253ee89e4a37536b35868a1a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#!/usr/bin/env python3
# Copyright 2020 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import re
import subprocess

import utils


GIT_LSTREE_RE_LINE = re.compile(rb'^([^ ]*) ([^ ]*) ([^ ]*)\t(.*)$')


class LazyTree:
    """LazyTree does git mktree lazily."""

    def __init__(self, treehash=None):
        """Initializes a LazyTree.

        If treehash is not None, it initializes as the tree object.

        Args:
            treehash: tree object id. please do not use a treeish, it will fail
                later.
        """
        if treehash:
            self._treehash = treehash # tree object id of current tree
            self._subtrees = None # map from directory name to sub LazyTree
            self._files = None # map from file naem to utils.GitFile
            return
        # Initialize an empty LazyTree
        self._treehash = None
        self._subtrees = {}
        self._files = {}

    def _loadtree(self):
        """Loads _treehash into _subtrees and _files."""
        if self._files is not None: # _subtrees is also not None too here.
            return
        output = subprocess.check_output(['git', 'ls-tree', self._treehash]).split(b'\n')
        self._files = {}
        self._subtrees = {}
        for line in output:
            if not line:
                continue
            m = GIT_LSTREE_RE_LINE.match(line)
            mode, gittype, objecthash, name = m.groups()
            assert gittype == b'blob' or gittype == b'tree'
            assert name not in self._files and name not in self._subtrees
            if gittype == b'blob':
                self._files[name] = utils.GitFile(None, mode, objecthash)
            elif gittype == b'tree':
                self._subtrees[name] = LazyTree(objecthash)

    def _remove(self, components):
        """Removes components from self tree.

        Args:
            components: the path to remove, relative to self. Each element means
                one level of directory tree.
        """
        self._loadtree()
        self._treehash = None
        if len(components) == 1:
            del self._files[components[0]]
            return

        # Remove from subdirectory
        dirname, components = components[0], components[1:]
        subdir = self._subtrees[dirname]
        subdir._remove(components)
        if subdir.is_empty():
            del self._subtrees[dirname]

    def __delitem__(self, path):
        """Removes path from self tree.

        Args:
            path: the path to remove, relative to self.
        """
        components = path.split(b'/')
        self._remove(components)

    def _get(self, components):
        """Returns a file at components in utils.GitFile from self tree.

        Args:
            components: path in list instead of separated by /.
        """
        self._loadtree()
        if len(components) == 1:
            return self._files[components[0]]

        dirname, components = components[0], components[1:]
        return self._subtrees[dirname]._get(components)

    def __getitem__(self, path):
        """Returns a file at path in utils.GitFile from tree.

        Args:
            path: path of the file to read.
        """
        components = path.split(b'/')
        return self._get(components)

    def _set(self, components, f):
        """Adds or replace a file.

        Args:
            components: the path to set, relative to self. Each element means
                one level of directory tree.
            f: a utils.GitFile object.
        """

        self._loadtree()
        self._treehash = None
        if len(components) == 1:
            self._files[components[0]] = f
            return

        # Add to subdirectory
        dirname, components = components[0], components[1:]
        if dirname not in self._subtrees:
            self._subtrees[dirname] = LazyTree()
        self._subtrees[dirname]._set(components, f)

    def __setitem__(self, path, f):
        """Adds or replaces a file.

        Args:
            path: the path to set, relative to self
            f: a utils.GitFile object
        """
        assert f.path.endswith(path)
        components = path.split(b'/')
        self._set(components, f)

    def is_empty(self):
        """Returns if self is an empty tree."""
        return not self._subtrees and not self._files

    def hash(self):
        """Returns the hash of current tree object.

        If the object doesn't exist, create it.
        """
        if not self._treehash:
            self._treehash = self._mktree()
        return self._treehash

    def _mktree(self):
        """Recreates a tree object recursively.

        Lazily if subtree is unchanged.
        """
        keys = list(self._files.keys()) + list(self._subtrees.keys())
        mktree_input = []
        for name in sorted(keys):
            file = self._files.get(name)
            if file:
                mktree_input.append(b'%s blob %s\t%s' % (file.mode, file.id,
                                                         name))
            else:
                mktree_input.append(
                    b'040000 tree %s\t%s' % (self._subtrees[name].hash(), name))
        return subprocess.check_output(
            ['git', 'mktree'],
            input=b'\n'.join(mktree_input)).strip(b'\n')