blob: 819e9afaae52289a6e44e287a44f2de87d2a7eb6 [file] [log] [blame] [edit]
#!/usr/bin/env python3
# Copyright 2020 The ChromiumOS Authors
# 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')