diff --git a/lib/wit/dependency.py b/lib/wit/dependency.py index 00341ea..5e903bd 100644 --- a/lib/wit/dependency.py +++ b/lib/wit/dependency.py @@ -77,7 +77,7 @@ def __eq__(self, other): def infer_name(source): return Path(source).name.replace('.git', '') - # NB: mutates packages[self.name] + # NB: mutates packages! def load(self, packages, repo_paths, wsroot, download): if self.name in packages: self.package = packages[self.name] @@ -131,12 +131,18 @@ def get_id(self): return "dep_"+re.sub(r"([^\w\d])", "_", self.tag()) def crawl_dep_tree(self, wsroot, repo_paths, packages): - fancy_tag = "{}::{}".format(self.name, self.short_revision()) self.load(packages, repo_paths, wsroot, False) + fancy_tag = "{}::{}".format(self.name, self.short_revision()) if self.package.repo is None: return {'': "{} \033[91m(missing)\033[m".format(fancy_tag)} - if self.package.revision != self.resolved_rev(): - fancy_tag += "->{}".format(self.package.short_revision()) + + different_name = self.package.name != self.name + if self.package.revision != self.resolved_rev() or different_name: + if different_name: + replacement = self.package.tag() + else: + replacement = self.package.short_revision() + fancy_tag += " -> {}".format(replacement) return {'': fancy_tag} tree = {'': fancy_tag} diff --git a/lib/wit/main.py b/lib/wit/main.py index 7724b44..7a47b4b 100644 --- a/lib/wit/main.py +++ b/lib/wit/main.py @@ -239,7 +239,7 @@ def add_dep(ws, args) -> None: if manifest_path.exists(): manifest = Manifest.read_manifest(manifest_path) else: - manifest = Manifest([]) + manifest = Manifest([], []) # make sure the dependency is not already in the cwd's manifest if manifest.contains_dependency(req_dep.name): diff --git a/lib/wit/manifest.py b/lib/wit/manifest.py index ae5ad3e..b015b00 100644 --- a/lib/wit/manifest.py +++ b/lib/wit/manifest.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import json +import collections from pathlib import Path from .witlogger import getLogger @@ -15,8 +16,9 @@ class Manifest: Common class for the description of package dependencies and a workspace """ - def __init__(self, dependencies): + def __init__(self, dependencies, replaces): self.dependencies = dependencies + self.replaces = replaces def get_dependency(self, name: str): for d in self.dependencies: @@ -58,16 +60,24 @@ def write(self, path): @staticmethod def read_manifest(path, safe=False): if safe and not Path(path).exists(): - return Manifest([]) + return Manifest([], []) content = json.loads(path.read_text()) return Manifest.process_manifest(content) @staticmethod def process_manifest(json_content): + replaces = [] + if isinstance(json_content, collections.Mapping): + if 'replaces' in json_content: + replaces = json_content['replaces'] + dep_specs = json_content['dependencies'] + else: + dep_specs = json_content + # import here to prevent circular dependency from .dependency import manifest_item_to_dep - dependencies = [manifest_item_to_dep(x) for x in json_content] - return Manifest(dependencies) + dependencies = [manifest_item_to_dep(x) for x in dep_specs] + return Manifest(dependencies, replaces) if __name__ == '__main__': diff --git a/lib/wit/package.py b/lib/wit/package.py index 941015d..59be4c3 100644 --- a/lib/wit/package.py +++ b/lib/wit/package.py @@ -36,6 +36,7 @@ def __init__(self, name, repo_paths): self.repo = None self.dependents = [] + self.replaces = [] def set_source(self, source): self.source = self.resolve_source(source) diff --git a/lib/wit/workspace.py b/lib/wit/workspace.py index 04dd78c..d92e55a 100644 --- a/lib/wit/workspace.py +++ b/lib/wit/workspace.py @@ -98,7 +98,7 @@ def create(cls, name, repo_paths): shutil.rmtree(str(dotwit)) dotwit.mkdir() - manifest = Manifest([]) + manifest = Manifest([], []) manifest.write(manifest_path) lockfile = LockFile([]) @@ -160,6 +160,32 @@ def resolve(self, download=False): source_map, packages, queue, errors = \ dep.resolve_deps(self.root, self.repo_paths, download, source_map, packages, queue, errors) + + replaces_map = {} + for name in packages: + pkg = packages[name] + if pkg.repo: + replaces_map[name] = pkg.repo.read_manifest_from_commit(pkg.revision).replaces + for parent_name in replaces_map: + parent = packages[parent_name] + child_names = replaces_map[parent_name] + if len(child_names) > 0: + for child_name in child_names: + if child_name not in packages: + log.debug("Did not find replaced child {}".format(child_name)) + continue + replaced = packages[child_name] + if not parent.repo.is_ancestor(replaced.revision, parent.revision): + error("I refuse to replace package '{}' with '{}' because \n" + "{} is not a git ancestor of {}".format(child_name, parent_name, + replaced.tag(), parent.tag())) + log.debug("Replacing {} with {}".format(child_name, parent_name)) + for dependent in replaced.dependents: + dependent.package = parent + parent.add_dependent(dependent) + + packages[child_name] = parent + return packages, errors @passbyval @@ -180,7 +206,15 @@ def resolve_deps(self, wsroot, repo_paths, download, source_map, packages, queue def checkout(self, packages): lock_packages = [] - for name in packages: + + for orig_name in packages: + pkg = packages[orig_name] + if orig_name != pkg.name and (self.root/orig_name).exists(): + log.warn("Package '{}' has been replaced by '{}', but the folder of '{}' still " + "exists".format(orig_name, pkg.name, orig_name)) + package_names = [packages[orig_name].name for orig_name in packages] + package_names = list(set(package_names)) + for name in package_names: package = packages[name] package.checkout(self.root) lock_packages.append(package) diff --git a/t/manifest_replaces.t b/t/manifest_replaces.t new file mode 100755 index 0000000..a360fd7 --- /dev/null +++ b/t/manifest_replaces.t @@ -0,0 +1,53 @@ +#!/bin/sh + +. $(dirname $0)/regress_util.sh + +prereq "on" + +# Set up repo foo +make_repo 'foo' +foo_commit=$(git -C foo rev-parse HEAD) + +cp -r foo newdir + +touch newdir/xyz +cat << EOF | jq . > newdir/wit-manifest.json +{ + "name": "foo-newname", + "replaces": ["foo"], + "dependencies": [] +} +EOF +git -C newdir add -A +git -C newdir commit -m "rename to foo-newdir" +foo_newname_commit=$(git -C newdir rev-parse HEAD) + +# Now set up repo main_repo to depend on foo, foo2, foo3 +mkdir main_repo +git -C main_repo init + +cat << EOF | jq . > main_repo/wit-manifest.json +[ + { "commit": "$foo_commit", "name": "foo", "source": "$PWD/foo" }, + { "commit": "$foo_newname_commit", "name": "foo-newname", "source": "$PWD/newdir" } +] +EOF + +git -C main_repo add -A +git -C main_repo commit -m "commit1" +main_repo_commit=$(git -C main_repo rev-parse HEAD) + +prereq "off" + +# Now create a workspace from main_repo +wit init myws -a $PWD/main_repo + +# Should fail because of conflicting paths for foo +check "wit init with conflicting paths passes" [ $? -eq 0 ] + + +wit --repo-path="$PWD $PWD/newdir $PWD/newdir2" init myws2 -a $PWD/main_repo +check "wit with path set succeeds" [ $? -eq 0 ] + +report +finish