Git Product home page Git Product logo

flexlate's Introduction

PyPI PyPI - License Documentation Tests Run on Ubuntu Python Versions Tests Run on Macos Python Versions Github Repo

flexlate

Overview

Flexlate is a composable, maintainable system for managing project and file generator templates.

Update your projects generated from cookiecutter and copier and compose projects from multiple templates.

Features

Use Cases

  • You want to create or already have projects that are generated from a cookiecutter or copier template, and keep those projects up to date with changes in the template
  • You want to create a project from standard building blocks that can also be updated systematically. For example think of something like a React component with tests, a Java class and tests, or any set of files you want to generate

Locally or Remote With a Team

In either case, you can use Flexlate 100% locally even on a team project without anyone else knowing you are using it via the user mode.

But Flexlate really shines when you embrace it fully and include it in your remote repo. This enables you use CI to automatically open PRs with template updates and merge Flexlate branches.

Why Flexlate?

Flexlate is born out of frustration with using project generator templates. You generate your project from a template, but later update the template and need to bring the changes back to your project. There are only a few tools for this and they do not have a great developer experience. Flexlate is Git-native, so you resolve template conflicts in Git as you would any other merge conflicts.

Further, there is not really any ability to compose a project template from smaller templates with any existing tools.

Check out a much more detailed explanation and story as well as a comparison to other tools.

How does it Work?

Flexlate is Git-native: it carries out all its operations via commits to Git branches. It maintains two branches, one that contains the history of the template output and the other than contains the merged output between your project and the template. This means that you resolve any conflicts with the template changes in Git and the merge conflict resolution is stored in the output branch.

It enables composability by using config files to keep track of where multiple templates should be rendered and with what data.

Learn more about Flexlate core concepts here.

Getting Started

Documentation

Visit the documentation for more detail on getting started. Start by learning about Flexlate core concepts before reading the user guide, which contains more detailed information on getting started.

Or, you can keep reading this high-level overview for abbreviated getting started steps.

Installing

Flexlate is a Python package that includes the fxt command line utility. If you do not have Python, you will need to install it first (required version is >=3.8).

The recommended way to install Flexlate is with pipx, though it can also be installed with pip.

pipx install flexlate

Or, if you don't have/don't want to install pipx:

pip install flexlate

Before using Flexlate, you will also need to have Git installed.

See the install guide for more information.

First Steps

Your first steps will depend on what you are trying to accomplish. See the "Next Steps" section of the installing guide for more information.

New Project from a Template

To generate a new project from a template, use init-from, e.g.:

fxt init-from https://github.com/nickderobertis/copier-pypi-sphinx-flexlate

See the user guide on creating a new project for more information.

Existing Project from a Template

To add Flexlate to your project that is already generated from a cookiecutter or cruft template, use bootstrap, e.g.:

fxt bootstrap https://github.com/nickderobertis/copier-pypi-sphinx-flexlate

See the user guide on adding Flexlate to an existing project from a template for more information.

Compose a Project from Multiple Templates

You can add a template source and then add as many outputs from that source as you want.

Before you can do this, you must initialize a Flexlate project:

fxt init

Then you can add the template source:

fxt add source https://github.com/nickderobertis/copier-pypi-sphinx-flexlate

Then you can apply the output anywhere in the project:

fxt add output copier-pypi-sphinx-flexlate

See the user guide on adding templates within an existing project for more information.

Updating a Template

See the user guide on updating a template for more information, but here's some quick info.

Re-prompt Questions

Once you have updates in the template that you want to bring to your project, use the update command:

fxt update

This will prompt for all the questions again, using your previous answers as defaults. If there are new questions from the update, or if you want to change any of the answers, you should follow this flow.

No Question Prompts

If instead you know that there are only changes in the outputs and not questions/answers, you can pass --no-input or -n to skip the questions:

fxt update -n

Saving your Work

See the user guide on saving Flexlate updates for more information, but here's some quick info.

Local Repo Flows

If you are following a local repo flow, then you can use the fxt merge command to merge the Flexlate feature branches into the Flexlate main branches. If you are using a feature-branch flow, then you would want to run fxt merge just before merging your feature branch into the main branch. If you are simply commititng to the main branch, just run fxt merge after any Flexlate command.

Remote Repo/PR Flows

If you are merging PRs in your repo rather than following a local flow, then you will want to fxt push feature just before/after your push your feature branch and open a PR. If you use the official Flexlate Github Merge Action, the Flexlate branches will be merged automatically after the PR is merged.

Get Help

You can run --help on the end of any command to see documentation. You will see similar output to what is in the command reference.

$ fxt --help
Usage: fxt [OPTIONS] COMMAND [ARGS]...

  fxt is a CLI tool to manage project and file generator templates.

  [See the Flexlate documentation](
  https://nickderobertis.github.io/flexlate/ ) for more information.

Options:
  -v, --version         Show Flexlate version and exit
  --install-completion  Install completion for the current shell.
  --show-completion     Show completion for the current shell, to copy it or
                        customize the installation.

  --help                Show this message and exit.

Commands:
  add        Add template sources and generate new projects and files from...
  bootstrap  Sets up a Flexlate project from an existing project that was...
  check      Checks whether there are any updates available for the current...
  config     Modify Flexlate configs via CLI
  init       Initializes a flexlate project.
  init-from  Generates a project from a template and sets it up as a...
  merge      Merges feature flexlate branches into the main flexlate...
  push       Push Flexlate branches to remote repositories.
  remove     Remove template sources and previously generated outputs
  sync       Syncs manual changes to the flexlate branches, and updates...
  undo       Undoes the last flexlate operation, like ctrl/cmd + z for...
  update     Updates applied templates in the project to the newest
             versions...

Please raise an issue if anything is confusing or does not work properly.

See a more in-depth tutorial here.

Development Status

This project is currently in early-stage development. There may be breaking changes often. While the major version is 0, minor version upgrades will often have breaking changes.

Developing

First ensure that you have pipx installed, if not, install it with pip install pipx.

Then clone the repo and run npm install and pipenv sync. Run pipenv shell to use the virtual environment. Make your changes and then run nox to run formatting, linting, and tests.

Develop documentation by running nox -s docs to start up a dev server.

To run tests only, run nox -s test. You can pass additional arguments to pytest by adding them after --, e.g. nox -s test -- -k test_something.

Author

Created by Nick DeRobertis. MIT License.

Links

See the documentation here.

flexlate's People

Contributors

github-actions[bot] avatar nickderobertis avatar semantic-release-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

flexlate's Issues

replace with cli command to update target version once it exists

replace with cli command to update target version once it exists

fxt(["add", "output", COOKIECUTTER_REMOTE_NAME], input_data=expect_data)

    )


def test_update_project(
    repo_with_placeholder_committed: Repo,
):
    repo = repo_with_placeholder_committed
    expect_data: CookiecutterRemoteTemplateData = dict(name="woo", key="it works")
    with change_directory_to(GENERATED_REPO_DIR):
        fxt("init")
        fxt(
            [
                "add",
                "source",
                COOKIECUTTER_REMOTE_URL,
                "--version",
                COOKIECUTTER_REMOTE_VERSION_1,
            ]
        )
        fxt(["add", "output", COOKIECUTTER_REMOTE_NAME], input_data=expect_data)
        _assert_project_files_are_correct(
            expect_data=expect_data, version=COOKIECUTTER_REMOTE_VERSION_1
        )
        _assert_config_is_correct(
            expect_data=expect_data, version=COOKIECUTTER_REMOTE_VERSION_1
        )
        fxt(["update", "--no-input"])
        # First update does nothing, because version is at target version
        _assert_project_files_are_correct(
            expect_data=expect_data, version=COOKIECUTTER_REMOTE_VERSION_1
        )
        _assert_config_is_correct(
            expect_data=expect_data, version=COOKIECUTTER_REMOTE_VERSION_1
        )
        # Now update the target version
        # TODO: replace with cli command to update target version once it exists
        config_path = GENERATED_REPO_DIR / "flexlate.json"
        config = FlexlateConfig.load(config_path)
        source = config.template_sources[0]
        source.target_version = COOKIECUTTER_REMOTE_VERSION_2
        config.save()
        stage_and_commit_all(
            repo, "Update target version for cookiecutter to version 2"
        )
        # Now update should go to new version
        fxt(["update", "--no-input"])

    _assert_project_files_are_correct(expect_data=expect_data)
    _assert_config_is_correct(expect_data=expect_data)

    project_config_path = GENERATED_REPO_DIR / "flexlate-project.json"
    _assert_project_config_is_correct(project_config_path, user=False)


def _assert_project_files_are_correct(
    root: Path = GENERATED_REPO_DIR,
    expect_data: Optional[CookiecutterRemoteTemplateData] = None,
    version: str = COOKIECUTTER_REMOTE_VERSION_2,
):
    data: CookiecutterRemoteTemplateData = expect_data or dict(name="abc", key="value")
    header = get_header_for_cookiecutter_remote_template(version)
    out_path = root / data["name"] / f"{data['name']}.txt"
    assert out_path.exists()
    content = out_path.read_text()
    assert content == f"{header}{data['key']}"


def _assert_template_sources_config_is_correct(
    config_path: Path = GENERATED_REPO_DIR / "flexlate.json",
    version: str = COOKIECUTTER_REMOTE_VERSION_2,
):
    assert config_path.exists()
    config = FlexlateConfig.load(config_path)

7e4f6bc7fa278b87dd594a18a9847aeaeed9479c

should this be use_branch_name and not base_branch_name?

should this be use_branch_name and not base_branch_name?

branch_names: Sequence[str],

    if not branch_exists(temp_repo, branch_name):
        # Now create the new branch
        log.debug(
            f"Creating {branch_name} in the temporary repo from branch {base_branch_name}"
        )
        # TODO: should this be use_branch_name and not base_branch_name?
        checkout_template_branch(temp_repo, branch_name, base_branch_name)

    return temp_repo

cc5408a211d50a914d46be95ebf60485d2155228

more efficient algorithm for updating locations of applied templates

more efficient algorithm for updating locations of applied templates

Currently it needs to find the template twice

# TODO: more efficient algorithm for updating locations of applied templates

            seen_sources.add(source.name)
        return list(sources_with_templates.values())

    def move_applied_template(
        self,
        template_name: str,
        config_path: Path,
        new_config_path: Path,
        project_root: Path = Path("."),
        out_root: Path = Path("."),
        orig_project_root: Path = Path("."),
    ):
        config = self.load_config(project_root=project_root, adjust_applied_paths=False)
        child_config = _get_or_create_child_config_by_path(config, config_path)
        template_index, _ = self._find_applied_template(
            template_name,
            config_path,
            project_root=project_root,
            out_root=out_root,
            orig_project_root=orig_project_root,
            config=config,
            adjust_applied_paths=False,
        )
        applied_template = child_config.applied_templates.pop(template_index)
        new_child_config = _get_or_create_child_config_by_path(config, new_config_path)
        new_child_config.applied_templates.append(applied_template)
        self.save_config(config)

    def move_template_source(
        self,
        template_name: str,
        config_path: Path,
        new_config_path: Path,
        project_root: Path = Path("."),
    ):
        config = self.load_config(project_root=project_root, adjust_applied_paths=False)
        child_config = _get_or_create_child_config_by_path(config, config_path)
        template_index, _ = self._find_template_source(
            template_name,
            config_path,
            project_root=project_root,
            config=config,
        )
        template_source = child_config.template_sources.pop(template_index)
        new_child_config = _get_or_create_child_config_by_path(config, new_config_path)
        new_child_config.template_sources.append(template_source)
        self.save_config(config)

    def _get_applied_templates_and_sources_with_local_add_mode(
        self,
        project_root: Path = Path("."),
        config: Optional[FlexlateConfig] = None,
    ) -> List[AppliedTemplateWithSource]:
        config = config or self.load_config(project_root=project_root)

        return [
            atws
            for atws in self.get_applied_templates_with_sources(
                project_root=project_root, config=config
            )
            if atws.applied_template.add_mode == AddMode.LOCAL
        ]

    def move_local_applied_templates_if_necessary(
        self,
        project_root: Path = Path("."),
        orig_project_root: Path = Path("."),
        renderer: MultiRenderer = MultiRenderer(),
    ):
        config = self.load_config(project_root=project_root)
        applied_templates_with_sources = (
            self._get_applied_templates_and_sources_with_local_add_mode(
                project_root=project_root, config=config
            )
        )
        for atwc in applied_templates_with_sources:
            source = atwc.source
            if source.is_local_template:
                # Move source back to orig project so that relative template
                # paths can be resolved
                source.path = str(
                    location_relative_to_new_parent(
                        Path(source.path), project_root, orig_project_root, project_root
                    ).resolve()
                )
            template = source.to_template()
            if template.render_relative_root_in_output == Path("."):
                # Should not need to move as config will not be in a subdirectory
                continue
            renderable = Renderable.from_applied_template_with_source(atwc)
            new_relative_out_root = Path(
                renderer.render_string(
                    str(template.render_relative_root_in_output), renderable
                )
            )
            if template.render_relative_root_in_output == new_relative_out_root:
                # No need to move, render relative root was not a templated path
                continue

            orig_config_path = atwc.applied_template._config_file_location

            render_root = (
                orig_config_path.parent / atwc.applied_template._orig_root
            ).resolve()
            new_config_path = render_root / new_relative_out_root / "flexlate.json"
            if orig_config_path == new_config_path:
                # No need to move, still in the same location
                continue

            # Must have different location now, move it
            # TODO: more efficient algorithm for updating locations of applied templates
            #  Currently it needs to find the template twice
            self.move_applied_template(
                atwc.source.name,
                orig_config_path,
                new_config_path,
                project_root=project_root,
                out_root=atwc.applied_template._orig_root,
                orig_project_root=orig_project_root,
            )


def _get_child_config_by_path(config: FlexlateConfig, path: Path) -> FlexlateConfig:
    for child_config in config.child_configs:

4add649c0601eb40a95db4abb8a09eb1b8599668

figure out why this fails in CI but not local

figure out why this fails in CI but not local

f"lefy only: {dirs_cmp.left_only}. right only: {dirs_cmp.right_only}. funny files: {dirs_cmp.funny_files}"

)

# TODO: figure out why this fails in CI but not local

            print(f"- {path} (directory)")
            if nested:
                display_contents_of_all_files_in_folder(path, nested)


def assert_dir_trees_are_equal(dir1: Union[str, Path], dir2: Union[str, Path]):
    """
    Compare two directories recursively. Files in each directory are
    assumed to be equal if their names and contents are equal.

    See: https://stackoverflow.com/a/6681395/6276321

    @param dir1: First directory path
    @param dir2: Second directory path

    @return: True if the directory trees are the same and
        there were no errors while accessing the directories or files,
        False otherwise.
    """

    dirs_cmp = filecmp.dircmp(dir1, dir2)
    if (
        len(dirs_cmp.left_only) > 0
        or len(dirs_cmp.right_only) > 0
        or len(dirs_cmp.funny_files) > 0
    ):
        pass
        # TODO: figure out why this fails in CI but not local

        # raise AssertionError(
        #     f"lefy only: {dirs_cmp.left_only}. right only: {dirs_cmp.right_only}. funny files: {dirs_cmp.funny_files}"
        # )
    (_, mismatch, errors) = filecmp.cmpfiles(
        dir1, dir2, dirs_cmp.common_files, shallow=False
    )
    if len(mismatch) > 0 or len(errors) > 0:
        raise AssertionError(f"mismatch: {mismatch}. errors: {errors}")
    for common_dir in dirs_cmp.common_dirs:
        new_dir1 = os.path.join(dir1, common_dir)
        new_dir2 = os.path.join(dir2, common_dir)
        assert_dir_trees_are_equal(new_dir1, new_dir2)

dafb247b3c09c4770ffd58ff909cfacf81006d6a

Manual Update to Files from Copier Needed

The template from the Copier that created this project must be updated using Flexlate.

Normally this is an automated process, but there were merge conflicts while applying the update.

Run pipenv run fxt update -n then manually review and update the changes, before pushing a PR
for this.

πŸ“ Some templates are not up to date. Run fxt update to update
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Template Name ┃ Current Version ┃ Latest Version ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ copier-pypi-sphinx-fle… β”‚ 9fe07eb96b8702c40f1248… β”‚ 89070159fd04c9efb3ccfb8… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Manual Update to Files from Copier Needed

The template from the Copier that created this project must be updated using Flexlate.

Normally this is an automated process, but there were merge conflicts while applying the update.

Run pipenv run fxt update -n then manually review and update the changes, before pushing a PR
for this.

πŸ“ Some templates are not up to date. Run fxt update to update
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Template Name ┃ Current Version ┃ Latest Version ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ copier-pypi-sphinx-fle… β”‚ b0916a7b0b576ebc6441d1… β”‚ 68fa0b20d416ea791a46bf1… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Manual Update to Files from Copier Needed

The template from the Copier that created this project must be updated using Flexlate.

Normally this is an automated process, but there were merge conflicts while applying the update.

Run pipenv run fxt update -n then manually review and update the changes, before pushing a PR
for this.

πŸ“ Some templates are not up to date. Run fxt update to update
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Template Name ┃ Current Version ┃ Latest Version ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ copier-pypi-sphinx-fle… β”‚ b0916a7b0b576ebc6441d1… β”‚ 68fa0b20d416ea791a46bf1… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Better strategy for finding earliest merge commit for transaction

Better strategy for finding earliest merge commit for transaction

The current strategy requires searching until the beginning of history.

Add early stopping when hitting a user commit or different flexlate transaction

# TODO: Better strategy for finding earliest merge commit for transaction

import uuid
from enum import Enum
from pathlib import Path
from typing import Optional, List, Sequence

from git import Repo, Commit  # type: ignore
from pydantic import BaseModel, Field, UUID4, validator

from flexlate.exc import (
    CannotParseCommitMessageFlexlateTransaction,
    LastCommitWasNotByFlexlateException,
    TransactionMismatchBetweenBranchesException,
    InvalidNumberOfTransactionsException,
    TooFewTransactionsException,
    ExpectedMergeCommitException,
    CannotFindCorrectMergeParentException,
    UserChangesWouldHaveBeenDeletedException,
    MergeCommitIsNotMergingAFlexlateTransactionException,
    CannotFindMergeForTransactionException,
)
from flexlate.ext_git import (
    reset_current_branch_to_commit,
    get_commits_between_two_commits,
)
from flexlate.template_data import TemplateData

FLEXLATE_TRANSACTION_COMMIT_DIVIDER = (
    "\n\n-------------------BEGIN FLEXLATE TRANSACTION-------------------\n"
)


class TransactionType(str, Enum):
    ADD_SOURCE = "add source"
    ADD_OUTPUT = "add output"
    REMOVE_SOURCE = "remove source"
    REMOVE_OUTPUT = "remove output"
    UPDATE = "update"


class FlexlateTransaction(BaseModel):
    type: TransactionType
    target: Optional[str] = None
    out_root: Optional[Path] = None
    data: Optional[Sequence[TemplateData]] = None
    id: UUID4 = Field(default_factory=lambda: uuid.uuid4())

    @validator("data", pre=True)
    def cast_data_into_sequence(cls, v):
        if isinstance(v, dict):
            return [v]
        return v

    @classmethod
    def parse_commit_message(cls, message: str) -> "FlexlateTransaction":
        parts = message.split(FLEXLATE_TRANSACTION_COMMIT_DIVIDER)
        if len(parts) != 2:
            raise CannotParseCommitMessageFlexlateTransaction(
                f"Could not parse commit message {message}. After splitting "
                f"on the divider, got {len(parts)} parts instead of 2"
            )
        _, transaction_part = parts
        return cls.parse_raw(transaction_part)

    @property
    def commit_message(self) -> str:
        return self.json(indent=2)


def create_transaction_commit_message(
    commit_message: str, transaction: FlexlateTransaction
) -> str:
    return (
        commit_message
        + FLEXLATE_TRANSACTION_COMMIT_DIVIDER
        + transaction.commit_message
    )


def reset_last_transaction(
    repo: Repo,
    transaction: FlexlateTransaction,
    merged_branch_name: str,
    template_branch_name: str,
):
    last_transaction = find_last_transaction_from_commit(
        repo.commit(), merged_branch_name, template_branch_name
    )
    if last_transaction.id != transaction.id:
        raise TransactionMismatchBetweenBranchesException(
            f"Found mismatching transaction ids: {last_transaction.id} and {transaction.id}"
        )
    earliest_commit = find_earliest_commit_that_was_part_of_transaction(
        repo, last_transaction, merged_branch_name, template_branch_name
    )

    is_template_branch = repo.active_branch.name == template_branch_name
    if is_template_branch:
        # On template branch, the only commits are flexlate transactions.
        # Therefore can just get the parent to find the commit before the
        # transaction started
        before_transaction_commit = _get_parent_commit(earliest_commit)
    else:
        # On output/user branch, typically the only commits are merging flexlate transactions
        # and user changes. So find the commit that merged this transaction,
        # then use its parent.
        try:
            merge_commit = find_earliest_merge_commit_for_transaction(
                repo, transaction, merged_branch_name, template_branch_name
            )
        except CannotFindMergeForTransactionException:
            # This is likely because the user has not made any changes in the repo yet,
            # and so the merges from the template branch are always fast-forwards.
            # In this case, it is a mirror of the template branch and so we should use
            # that logic
            before_transaction_commit = _get_parent_commit(earliest_commit)
        else:
            before_transaction_commit = (
                _get_non_flexlate_transaction_parent_from_flexlate_merge_commit(
                    merge_commit, merged_branch_name, template_branch_name
                )
            )

    assert_that_all_commits_between_two_are_flexlate_transactions_or_merges(
        repo,
        before_transaction_commit,
        repo.commit(),
        merged_branch_name,
        template_branch_name,
    )
    reset_current_branch_to_commit(repo, before_transaction_commit)


def find_last_transaction_from_commit(
    commit: Commit, merged_branch_name: str, template_branch_name: str
) -> FlexlateTransaction:
    if _is_flexlate_merge_commit(commit, merged_branch_name, template_branch_name):
        parent = _get_flexlate_transaction_parent_from_flexlate_merge_commit(
            commit, merged_branch_name, template_branch_name
        )
        return find_last_transaction_from_commit(
            parent, merged_branch_name, template_branch_name
        )
    return FlexlateTransaction.parse_commit_message(commit.message)


def find_earliest_merge_commit_for_transaction(
    repo: Repo,
    transaction: FlexlateTransaction,
    merged_branch_name: str,
    template_branch_name: str,
) -> Commit:
    # Walk back through commit tree, searching for the appropriate commit
    return _search_commit_tree_for_earliest_merge_commit_for_transaction(
        repo.commit(), transaction, merged_branch_name, template_branch_name
    )


def _search_commit_tree_for_earliest_merge_commit_for_transaction(
    commit: Commit,
    transaction: FlexlateTransaction,
    merged_branch_name: str,
    template_branch_name: str,
):
    # TODO: Better strategy for finding earliest merge commit for transaction
    #  The current strategy requires searching until the beginning of history.
    #  Add early stopping when hitting a user commit or different flexlate transaction
    search_commits = [commit]
    found_commits: List[Commit] = []
    # Breadth-first search
    while len(search_commits) > 0:
        this_commit = search_commits.pop(0)
        if _is_flexlate_merge_commit(
            this_commit, merged_branch_name, template_branch_name
        ):
            merge_transaction = _get_transaction_underlying_merge_commit(this_commit)
            if transaction.id == merge_transaction.id:
                found_commits.append(this_commit)
        search_commits.extend(list(this_commit.parents))

    if len(found_commits) == 0:
        raise CannotFindMergeForTransactionException(
            f"Could not find the merge commit for transaction {transaction}"
        )

    # Since BFS was used, last found commit should be the earliest
    return found_commits[-1]


def _get_transaction_underlying_merge_commit(commit: Commit) -> FlexlateTransaction:
    for parent in commit.parents:
        try:
            return FlexlateTransaction.parse_commit_message(parent.message)
        except CannotParseCommitMessageFlexlateTransaction:
            continue
    raise MergeCommitIsNotMergingAFlexlateTransactionException(
        f"Commit {commit.hexsha}: {commit.message}"
    )


def assert_last_commit_was_in_a_flexlate_transaction(
    repo: Repo, merged_branch_name: str, template_branch_name: str
):
    last_commit = repo.commit()
    if _is_flexlate_merge_commit(last_commit, merged_branch_name, template_branch_name):
        return
    if isinstance(last_commit.message, bytes):
        raise LastCommitWasNotByFlexlateException(
            f"Last commit was not made by flexlate, message is bytes"
        )
    try:

        FlexlateTransaction.parse_commit_message(last_commit.message)
    except CannotParseCommitMessageFlexlateTransaction as e:
        raise LastCommitWasNotByFlexlateException(
            f"Last commit was not made by flexlate: {last_commit.message}"
        ) from e


def assert_has_at_least_n_transactions(
    repo: Repo, n: int, merged_branch_name: str, template_branch_name: str
):
    if n < 0:
        raise InvalidNumberOfTransactionsException(
            "Number of transactions must be positive"
        )
    assert_last_commit_was_in_a_flexlate_transaction(
        repo, merged_branch_name, template_branch_name
    )
    if n == 1:
        return
    last_commit = repo.commit()
    num_to_verify = n

    def too_few_transactions():
        # Have hit the end but have not finished verifying, therefore there are too few transactions
        raise TooFewTransactionsException(
            f"Desired to undo {n} transactions but only found {n - num_to_verify}"
        )

    for _ in range(num_to_verify):
        if _is_flexlate_merge_commit(
            last_commit, merged_branch_name, template_branch_name
        ):
            last_commit = _get_flexlate_transaction_parent_from_flexlate_merge_commit(
                last_commit, merged_branch_name, template_branch_name
            )
        if isinstance(last_commit.message, bytes):
            # Flexlate never commits with binary messages
            return too_few_transactions()
        try:
            transaction = FlexlateTransaction.parse_commit_message(last_commit.message)
        except CannotParseCommitMessageFlexlateTransaction:
            return too_few_transactions()
        earliest_commit = _return_commit_if_begin_of_transaction_else_get_parent(
            last_commit, transaction, merged_branch_name, template_branch_name
        )
        num_to_verify -= 1
        if num_to_verify <= 0:
            return
        try:
            last_commit = _get_parent_commit(earliest_commit)
        except (HitInitialCommit, HitMergeCommit):
            return too_few_transactions()


def assert_that_all_commits_between_two_are_flexlate_transactions_or_merges(
    repo: Repo,
    start: Commit,
    end: Commit,
    merged_branch_name: str,
    template_branch_name: str,
):
    between_commits = get_commits_between_two_commits(repo, start, end)
    for commit in between_commits:
        if _is_flexlate_merge_commit(commit, merged_branch_name, template_branch_name):
            continue
        try:
            FlexlateTransaction.parse_commit_message(commit.message)
        except CannotParseCommitMessageFlexlateTransaction:
            raise UserChangesWouldHaveBeenDeletedException(
                f"Commit {commit.hexsha}: {commit.message} would have been deleted "
                f"by the flexlate undo strategy. An extra check prevented it. "
                f"Please raise this as an issue on Github"
            )


def find_earliest_commit_that_was_part_of_transaction(
    repo: Repo,
    transaction: FlexlateTransaction,
    merged_branch_name: str,
    template_branch_name: str,
) -> Commit:
    return _return_commit_if_begin_of_transaction_else_get_parent(
        repo.commit(), transaction, merged_branch_name, template_branch_name
    )


def _return_commit_if_begin_of_transaction_else_get_parent(
    commit: Commit,
    transaction: FlexlateTransaction,
    merged_branch_name: str,
    template_branch_name: str,
) -> Commit:
    try:
        parent_commit = _get_parent_commit(commit)
    except HitInitialCommit:
        return commit
    except HitMergeCommit:
        parent_commit = _get_flexlate_transaction_parent_from_flexlate_merge_commit(
            commit, merged_branch_name, template_branch_name
        )
    try:
        commit_transaction = FlexlateTransaction.parse_commit_message(
            parent_commit.message
        )
    except CannotParseCommitMessageFlexlateTransaction:
        # Not a flexlate commit, so this must be the last in the transaction
        return commit

    if commit_transaction.id == transaction.id:
        # Parent is still in the same transaction. Recurse to find the original commit
        return _return_commit_if_begin_of_transaction_else_get_parent(
            parent_commit, transaction, merged_branch_name, template_branch_name
        )

    # It is a flexlate commit, but different transaction. Therefore return this commit
    return commit


class HitInitialCommit(Exception):
    pass


class HitMergeCommit(Exception):
    pass


def _get_parent_commit(commit: Commit) -> Commit:
    parents = commit.parents
    if len(parents) == 0:
        raise HitInitialCommit
    if len(parents) > 1:
        raise HitMergeCommit
    return parents[0]


def _get_flexlate_transaction_parent_from_flexlate_merge_commit(
    commit: Commit, merged_branch_name: str, template_branch_name: str
) -> Commit:
    if not _is_flexlate_merge_commit(commit, merged_branch_name, template_branch_name):
        raise ExpectedMergeCommitException("not a flexlate merge commit")
    if len(commit.parents) != 2:
        raise ExpectedMergeCommitException(f"has {len(commit.parents)} parents, not 2")
    flexlate_transaction_parents: List[Commit] = []
    for parent in commit.parents:
        try:
            FlexlateTransaction.parse_commit_message(parent.message)
            flexlate_transaction_parents.append(parent)
        except CannotParseCommitMessageFlexlateTransaction:
            pass
    if len(flexlate_transaction_parents) != 1:
        raise CannotFindCorrectMergeParentException(
            f"Cannot determine which of these commits is the flexlate transaction parent {flexlate_transaction_parents}"
        )
    return flexlate_transaction_parents[0]


def _get_non_flexlate_transaction_parent_from_flexlate_merge_commit(
    commit: Commit, merged_branch_name: str, template_branch_name: str
) -> Commit:
    if not _is_flexlate_merge_commit(commit, merged_branch_name, template_branch_name):
        raise ExpectedMergeCommitException("not a flexlate merge commit")
    if len(commit.parents) != 2:
        raise ExpectedMergeCommitException(f"has {len(commit.parents)} parents, not 2")
    non_flexlate_transaction_parents: List[Commit] = []
    for parent in commit.parents:
        try:
            FlexlateTransaction.parse_commit_message(parent.message)
        except CannotParseCommitMessageFlexlateTransaction:
            non_flexlate_transaction_parents.append(parent)
    if len(non_flexlate_transaction_parents) != 1:
        raise CannotFindCorrectMergeParentException(
            f"Cannot determine which of these commits is the non-flexlate transaction parent {non_flexlate_transaction_parents}"
        )
    return non_flexlate_transaction_parents[0]


def _is_flexlate_merge_commit(
    commit: Commit, merged_branch_name: str, template_branch_name: str
) -> bool:
    return (
        commit.message
        == f"Merge branch '{template_branch_name}' into {merged_branch_name}\n"
    )

0ce5fe15b0f6d7c7a0e04b7e877d353428e04f2f

figure out how to test cloning SSH urls

figure out how to test cloning SSH urls

# TODO: figure out how to test cloning SSH urls

from pathlib import Path

import pytest

from flexlate.exc import InvalidTemplatePathException
from flexlate.template_path import (
    is_repo_url,
    is_local_template,
    get_local_repo_path_and_name_cloning_if_repo_url,
)
from tests.config import GENERATED_FILES_DIR
from tests.dirutils import wipe_generated_folder
from tests.fixtures.repo_path import (
    repo_path_fixture,
    repo_path_non_ssh_fixture,
    RepoPathFixture,
)


def test_is_repo_url(repo_path_fixture: RepoPathFixture):
    assert is_repo_url(repo_path_fixture.path) == repo_path_fixture.is_repo_url


def test_is_local_path(repo_path_fixture: RepoPathFixture):
    assert is_local_template(repo_path_fixture.path) == repo_path_fixture.is_local_path


# TODO: figure out how to test cloning SSH urls
def test_get_local_repo_path_cloning_if_repo_url(
    repo_path_non_ssh_fixture: RepoPathFixture,
):
    repo_path_fixture = repo_path_non_ssh_fixture
    wipe_generated_folder()
    if not repo_path_fixture.is_repo_url and not repo_path_fixture.is_local_path:
        # Invalid path test
        with pytest.raises(InvalidTemplatePathException):
            get_local_repo_path_and_name_cloning_if_repo_url(
                repo_path_fixture.path, GENERATED_FILES_DIR
            )
        # Path was invalid so nothing else to check, end test
        return

    # Must be valid template path, local or remote
    for version in repo_path_fixture.versions:
        local_path, name = get_local_repo_path_and_name_cloning_if_repo_url(
            repo_path_fixture.path, version, GENERATED_FILES_DIR
        )
        assert name == repo_path_fixture.name
        if repo_path_fixture.is_local_path:
            assert local_path == Path(repo_path_fixture.path)
        elif repo_path_fixture.is_repo_url:
            assert local_path == GENERATED_FILES_DIR / repo_path_fixture.name / (
                version or repo_path_fixture.default_version
            )
            repo_path_fixture.assert_was_cloned_correctly(local_path, version)
        wipe_generated_folder()

01e3bed3af955a208fc8f6edf0db0de9fe66f8a3

Avoid unnecessary git repo cloning

Avoid unnecessary git repo cloning

We already know that we have it by this point, but need to get the local path

and the logic to resolve the version that may be None is entertwined with the

cloning and local path determination

updating templates, it can update the path without forcing it to be absolute

# TODO: Avoid unnecessary git repo cloning

            kwargs.update(target_version=self.target_version)
        if self.git_url is not None:
            kwargs.update(git_url=self.git_url)
        local_path: Path
        if self.git_url is not None:
            # TODO: Avoid unnecessary git repo cloning
            #  We already know that we have it by this point, but need to get the local path
            #  and the logic to resolve the version that may be None is entertwined with the
            #  cloning and local path determination
            local_path, _ = get_local_repo_path_and_name_cloning_if_repo_url(
                self.git_url, version=self.version
            )
        else:
            local_path = self.absolute_local_path

        template = finder.find(self.git_url or str(local_path), local_path, **kwargs)
        # Keep original template source path (may be relative), so that later when
        # updating templates, it can update the path without forcing it to be absolute

0a50065575bb9afe9d45f61a3a31cac3810ece20

Manual Update to Files from Copier Needed

The template from the Copier that created this project must be updated using Flexlate.

Normally this is an automated process, but there were merge conflicts while applying the update.

Run pipenv run fxt update -n then manually review and update the changes, before pushing a PR
for this.

πŸ“ Some templates are not up to date. Run fxt update to update
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Template Name ┃ Current Version ┃ Latest Version ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ copier-pypi-sphinx-fle… β”‚ b0916a7b0b576ebc6441d1… β”‚ 68fa0b20d416ea791a46bf1… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Manual Update to Files from Copier Needed

The template from the Copier that created this project must be updated using Flexlate.

Normally this is an automated process, but there were merge conflicts while applying the update.

Run pipenv run fxt update -n then manually review and update the changes, before pushing a PR
for this.

πŸ“ Some templates are not up to date. Run fxt update to update
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Template Name ┃ Current Version ┃ Latest Version ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ copier-pypi-sphinx-fle… β”‚ b0916a7b0b576ebc6441d1… β”‚ 68fa0b20d416ea791a46bf1… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Manual Update to Files from Copier Needed

The template from the Copier that created this project must be updated using Flexlate.

Normally this is an automated process, but there were merge conflicts while applying the update.

Run pipenv run fxt update -n then manually review and update the changes, before pushing a PR
for this.

πŸ“ Some templates are not up to date. Run fxt update to update
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Template Name ┃ Current Version ┃ Latest Version ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ copier-pypi-sphinx-fle… β”‚ b0916a7b0b576ebc6441d1… β”‚ 68fa0b20d416ea791a46bf1… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Fix multiple iterations over files for git traverse

Fix multiple iterations over files for git traverse

For now just using a set to keep it working, but should optimize

# TODO: Fix multiple iterations over files for git traverse

    branch.checkout()


def _get_initial_commit_sha(repo: Repo) -> str:
    return repo.git.rev_list("HEAD", max_parents=0)


def _get_initial_commit(repo: Repo) -> Commit:
    return repo.commit(_get_initial_commit_sha(repo))


def stage_and_commit_all(repo: Repo, commit_message: str):
    repo.git.add("-A")
    repo.git.commit("-m", commit_message)


def list_tracked_files(repo: Repo) -> Set[Path]:
    if repo.working_dir is None:
        raise ValueError("repo working dir should not be none")
    return _list_tracked_files(repo.tree(), Path(repo.working_dir))


def _list_tracked_files(tree: Tree, root_path: Path) -> Set[Path]:
    # TODO: Fix multiple iterations over files for git traverse
    #  For now just using a set to keep it working, but should optimize
    files: Set[Path] = set()
    for tree_or_blob in tree.traverse():
        if hasattr(tree_or_blob, "traverse"):
            # Got another tree
            tree = cast(Tree, tree_or_blob)
            files.update(_list_tracked_files(tree, root_path))
        else:
            # Got a blob
            blob = cast(Blob, tree_or_blob)
            files.add(root_path / Path(blob.path))
    return files


def delete_tracked_files_excluding_initial_commit(repo: Repo):
    if repo.working_dir is None:
        raise ValueError("repo working dir should not be none")
    initial_commit_files = _list_tracked_files(
        _get_initial_commit(repo).tree, Path(repo.working_dir)
    )
    for path in list_tracked_files(repo):
        if path not in initial_commit_files:
            os.remove(path)


def merge_branch_into_current(
    repo: Repo, branch_name: str, allow_conflicts: bool = True
):

9bca34bbb51859ded81a6573a232aa94b97381f2

tests for different out roots

tests for different out roots

# TODO: tests for different out roots

    assert at.name == template.name
    assert at.version == template.version
    assert at.data == {"name": "abc", "key": "value"}
    assert at.root == template_root


# TODO: tests for different out roots
# TODO: test for adding to existing

00b0f115859524a4050eadfb8a5c654a81df1fad

determine why passing target_version through kwargs was not necessary for copier

determine why passing target_version through kwargs was not necessary for copier

Had to do that for cookiecutter, but tests were passing without any changes here.

# TODO: determine why passing target_version through kwargs was not necessary for copier

class CopierFinder(TemplateFinder[CopierTemplate]):
    def find(self, path: str, local_path: Path, **template_kwargs) -> CopierTemplate:
        # TODO: determine why passing target_version through kwargs was not necessary for copier
        #  Had to do that for cookiecutter, but tests were passing without any changes here.
        git_version: Optional[str] = template_kwargs.get("version")
        custom_name: Optional[str] = template_kwargs.get("name")
        name = custom_name or local_path.name
        config = self.get_config(local_path)
        version = get_version_from_source_path(path, local_path) or git_version
        git_url = get_git_url_from_source_path(path, template_kwargs)
        return CopierTemplate(
            config,
            local_path,
            name=name,
            version=version,
            target_version=git_version,

9237ce8013380f22c666f40a70552978ca4b4cae

Make CLI have a pause and resume capability

Make CLI have a pause and resume capability

We should be running these post actions below afterwards regardless of whether

there was a merge conflict.

and there was nothing else in the folder (git does not save folders without files)

need to set cwd again

# TODO: Make CLI have a pause and resume capability

            current_branch.checkout()
            merge_branch_into_current(repo, merged_branch_name)

            # TODO: Make CLI have a pause and resume capability
            #  We should be running these post actions below afterwards regardless of whether
            #  there was a merge conflict.

            # Current working directory or out root may have been deleted if it was a remove operation
            # and there was nothing else in the folder (git does not save folders without files)
            ensure_exists_folders = [cwd]
            for renderable in orig_renderables:
                ensure_exists_folders.append(
                    make_absolute_path_from_possibly_relative_to_another_path(
                        renderable.out_root, project_root
                    )
                )
            for folder in ensure_exists_folders:
                if not os.path.exists(folder):
                    os.makedirs(folder)

            # Folder may have been deleted again while switching branches, so
            # need to set cwd again
            os.chdir(cwd)

    def get_updates_for_templates(
        self,
        templates: Sequence[Template],

e90686dc62e002a7918c6a630635a2cecebbc322

see if there is a better way to structure multiple parameterized fixtures

see if there is a better way to structure multiple parameterized fixtures

that actually resolve to the same value

# TODO: see if there is a better way to structure multiple parameterized fixtures

from enum import Enum
from typing import List, Final

import pytest
from git import Repo

from flexlate.ext_git import delete_local_branch
from tests.gitutils import reset_n_commits_without_checkout


class LocalBranchSituation(str, Enum):
    UP_TO_DATE = "up to date"
    DELETED = "deleted"
    OUT_OF_DATE = "out of date"

    def apply(self, repo: Repo, branch_name: str):
        apply_local_branch_situation(repo, branch_name, self)


all_local_branch_situations: Final[List[LocalBranchSituation]] = list(
    LocalBranchSituation
)


@pytest.fixture(scope="module", params=all_local_branch_situations)
def local_branch_situation(request) -> LocalBranchSituation:
    return request.param


# TODO: see if there is a better way to structure multiple parameterized fixtures
#  that actually resolve to the same value
@pytest.fixture(scope="module", params=all_local_branch_situations)
def template_branch_situation(request) -> LocalBranchSituation:
    return request.param


@pytest.fixture(scope="module", params=all_local_branch_situations)
def output_branch_situation(request) -> LocalBranchSituation:
    return request.param


def apply_local_branch_situation(
    repo: Repo, branch_name: str, situation: LocalBranchSituation
):
    if situation == LocalBranchSituation.UP_TO_DATE:
        return
    if situation == LocalBranchSituation.DELETED:
        return delete_local_branch(repo, branch_name)
    if situation == LocalBranchSituation.OUT_OF_DATE:
        return reset_n_commits_without_checkout(repo, branch_name)
    raise NotImplementedError(f"no handling for local branch situation {situation}")

7f058873b21c67c2af09f64137c33b4d0d58bbf9

restructure the current path logic into _add_operation_via_branches

restructure the current path logic into _add_operation_via_branches

as it will also be needed for other operations.

Commit changes for local and project

or os.getcwd will throw a FileNotExistsError (which also breaks path.absolute())

# TODO: restructure the current path logic into _add_operation_via_branches

                out_root=expanded_out_root,
            )
        else:
            # TODO: restructure the current path logic into _add_operation_via_branches
            #  as it will also be needed for other operations.
            # Commit changes for local and project
            cwd = Path(os.getcwd())
            absolute_out_root = out_root.absolute()
            def make_dirs_add_applied_template():
                # Need to ensure that both cwd and out root exist on the template branch
                for p in [cwd, absolute_out_root]:
                    if not p.exists():
                        p.mkdir(parents=True)
                # If cwd was deleted when switching branches, need to navigate back there
                # or os.getcwd will throw a FileNotExistsError (which also breaks path.absolute())
                os.chdir(cwd)

                config_manager.add_applied_template(
                    template,
                    config_path,
                    data=data,
                    project_root=project_root,
                    out_root=expanded_out_root,
                )

            _add_operation_via_branches(
                make_dirs_add_applied_template,
                repo,
                _add_template_commit_message(
                    template, out_root, Path(repo.working_dir)

2289cc96ea4bf20fbc193289a97fa0e79fc2e2bd

make sure it's the right git error

make sure it's the right git error

Could not fast forward. Must do a merge and have user resolve any conflicts

# TODO: make sure it's the right git error

from typing import Optional

from git import Repo, GitCommandError

from flexlate.branch_update import (
    get_flexlate_branch_name_for_feature_branch,
    abort_merge_and_reset_flexlate_branches,
)
from flexlate.cli_utils import confirm_user
from flexlate.constants import DEFAULT_MERGED_BRANCH_NAME, DEFAULT_TEMPLATE_BRANCH_NAME
from flexlate.ext_git import (
    fast_forward_branch_without_checkout,
    temp_repo_that_pushes_to_branch,
    repo_has_merge_conflicts,
    get_branch_sha,
    delete_local_branch,
)
from flexlate.styles import (
    print_styled,
    INFO_STYLE,
    ACTION_REQUIRED_STYLE,
    styled,
    QUESTION_STYLE,
    ALERT_STYLE,
    SUCCESS_STYLE,
)


class Merger:
    def merge_flexlate_branches(
        self,
        repo: Repo,
        branch_name: Optional[str] = None,
        delete: bool = True,
        merged_branch_name: str = DEFAULT_MERGED_BRANCH_NAME,
        template_branch_name: str = DEFAULT_TEMPLATE_BRANCH_NAME,
    ):
        feature_branch = branch_name or repo.active_branch.name
        flexlate_feature_merged_branch_name = (
            get_flexlate_branch_name_for_feature_branch(
                feature_branch, merged_branch_name
            )
        )
        flexlate_feature_template_branch_name = (
            get_flexlate_branch_name_for_feature_branch(
                feature_branch, template_branch_name
            )
        )

        # Save the status of the flexlate branches. We may need to roll back to this state
        # if the user aborts the merge
        current_branch = repo.active_branch
        merged_branch_sha = get_branch_sha(repo, merged_branch_name)
        template_branch_sha = get_branch_sha(repo, template_branch_name)

        print_styled(
            f"Merging {flexlate_feature_template_branch_name} to {template_branch_name}",
            INFO_STYLE,
        )
        try:
            fast_forward_branch_without_checkout(
                repo, template_branch_name, flexlate_feature_template_branch_name
            )
        except GitCommandError as e:
            # TODO: make sure it's the right git error
            # Could not fast forward. Must do a merge in a temp repo and have user resolve any conflicts
            with temp_repo_that_pushes_to_branch(  # type: ignore
                repo,
                branch_name=template_branch_name,
                base_branch_name=template_branch_name,
                additional_branches=(flexlate_feature_template_branch_name,),
            ) as temp_repo:
                temp_repo.git.merge(flexlate_feature_template_branch_name)  # type: ignore
                if repo_has_merge_conflicts(temp_repo):
                    print_styled(
                        f"Encountered merge conflicts while merging "
                        f"{flexlate_feature_template_branch_name} into {template_branch_name}",
                        INFO_STYLE,
                    )
                    print_styled(
                        f"Flexlate uses a temporary repo for this merge. "
                        f"Please resolve conflicts in {temp_repo.working_dir}",  # type: ignore
                        ACTION_REQUIRED_STYLE,
                    )
                    handled_conflicts = confirm_user(
                        styled(
                            "Successfully handled conflicts? n to abort", QUESTION_STYLE
                        )
                    )
                    if not handled_conflicts:
                        # Have only done work in a temp dir so far, so can just abort
                        print_styled("Aborting merge.", ALERT_STYLE)
                        return
        print_styled(
            f"Successfully merged {flexlate_feature_template_branch_name} to {template_branch_name}",
            SUCCESS_STYLE,
        )

        # Now merge the merged branch in the main repo
        try:
            fast_forward_branch_without_checkout(
                repo, merged_branch_name, flexlate_feature_merged_branch_name
            )
        except GitCommandError as e:
            # TODO: make sure it's the right git error
            # Could not fast forward. Must do a merge and have user resolve any conflicts
            merged_branch = repo.branches[merged_branch_name]  # type: ignore
            merged_branch.checkout()
            repo.git.merge(flexlate_feature_merged_branch_name)
            if repo_has_merge_conflicts(temp_repo):
                print_styled(
                    f"Encountered merge conflicts while merging "
                    f"{flexlate_feature_template_branch_name} into {template_branch_name}",
                    INFO_STYLE,
                )
                print_styled(f"Please resolve the conflicts", ACTION_REQUIRED_STYLE)
                handled_conflicts = confirm_user(
                    styled("Successfully handled conflicts? n to abort", QUESTION_STYLE)
                )
                if not handled_conflicts:
                    # Time ot abort, but need to reset the state of the branches
                    print_styled("Aborting merge.", ALERT_STYLE)
                    abort_merge_and_reset_flexlate_branches(
                        repo,
                        current_branch,
                        merged_branch_sha=merged_branch_sha,
                        template_branch_sha=template_branch_sha,
                        merged_branch_name=merged_branch_name,
                        template_branch_name=template_branch_name,
                    )

                    return

        print_styled(
            f"Successfully merged {flexlate_feature_merged_branch_name} to {merged_branch_name}",
            SUCCESS_STYLE,
        )

        if not delete:
            return

        # Handle delete
        print_styled(
            f"Deleting flexlate feature branches {flexlate_feature_template_branch_name} and {flexlate_feature_merged_branch_name}",
            INFO_STYLE,
        )
        delete_local_branch(repo, flexlate_feature_template_branch_name)
        delete_local_branch(repo, flexlate_feature_merged_branch_name)
        print_styled(f"Successfully deleted flexlate feature branches", SUCCESS_STYLE)

24464e762b46e19a1500c58243a696e37bfd6baa

replace with cli command to update target version once it exists

replace with cli command to update target version once it exists

# TODO: replace with cli command to update target version once it exists

    _assert_project_config_is_correct(project_config_path, user=False)


def _assert_root_template_output_is_correct(
    template_source: TemplateSourceFixture,
    after_version_update: bool = False,
    after_data_update: bool = False,
    num_template_sources: int = 1,
    template_source_index: int = 0,
):
    version = (
        template_source.version_2 if after_version_update else template_source.version_1
    )
    input_data = (
        template_source.update_input_data
        if after_data_update
        else template_source.input_data
    )
    at_config_path = (
        GENERATED_REPO_DIR
        / template_source.evaluated_render_relative_root_in_output_creator(input_data)
        / "flexlate.json"
    )
    _assert_project_files_are_correct(
        expect_data=input_data,
        version=version,
        template_source_type=template_source.type,
    )
    _assert_config_is_correct(
        at_config_path=at_config_path,
        expect_applied_template_root=template_source.expect_local_applied_template_path,
        expect_data=input_data,
        version=version,
        template_source_type=template_source.type,
        name=template_source.name,
        url=template_source.url,
        path=template_source.path,
        render_relative_root_in_output=template_source.render_relative_root_in_output,
        render_relative_root_in_template=template_source.render_relative_root_in_template,
        num_template_sources=num_template_sources,
        template_source_index=template_source_index,
    )


def test_update_one_template(
    flexlates: FlexlateFixture,
    repo_with_placeholder_committed: Repo,
):
    fxt = flexlates.flexlate
    repo = repo_with_placeholder_committed
    no_input = flexlates.type == FlexlateType.APP

    def assert_root_template_output_is_correct(
        template_source: TemplateSourceFixture,
        after_version_update: bool = False,
        after_data_update: bool = False,
        num_template_sources: int = 2,
        template_source_index: int = 0,
    ):
        _assert_root_template_output_is_correct(
            template_source,
            after_version_update,
            after_data_update,
            num_template_sources=num_template_sources,
            template_source_index=template_source_index,
        )

    non_update_template_source: TemplateSourceFixture = COOKIECUTTER_REMOTE_FIXTURE
    with template_source_with_temp_dir_if_local_template(
        COPIER_LOCAL_FIXTURE
    ) as template_source:
        with change_directory_to(GENERATED_REPO_DIR):
            fxt.init_project()
            # Add both template sources and outputs in the main directory at version 1
            for i, ts in enumerate([template_source, non_update_template_source]):
                fxt.add_template_source(ts.path, target_version=ts.version_1)
                fxt.apply_template_and_add(
                    ts.name, data=ts.input_data, no_input=no_input
                )
                num_template_sources = i + 1
                assert_root_template_output_is_correct(
                    ts,
                    num_template_sources=num_template_sources,
                    template_source_index=i,
                )

            # First update does nothing, because version is at target version
            # When using app, it will throw an error
            if flexlates.type == FlexlateType.APP:
                with pytest.raises(TriedToCommitButNoChangesException) as excinfo:
                    fxt.update([template_source.name], no_input=True)
                assert "update did not make any new changes" in str(excinfo.value)
            else:
                # When using CLI stub, it will throw a CLIRunnerException
                with pytest.raises(CLIRunnerException) as excinfo:
                    fxt.update([template_source.name], no_input=True)
                assert "update did not make any new changes" in str(excinfo.value)

            assert_root_template_output_is_correct(template_source)
            assert_root_template_output_is_correct(
                non_update_template_source, template_source_index=1
            )

            # Now update by just passing new data, should change the output
            # even though the version has not changed
            fxt.update(
                [template_source.name],
                data=[template_source.update_input_data],
                no_input=no_input,
            )
            assert_root_template_output_is_correct(
                template_source, after_data_update=True
            )
            # Other template unaffected
            assert_root_template_output_is_correct(
                non_update_template_source, template_source_index=1
            )

            # Now update the target version
            # TODO: replace with cli command to update target version once it exists
            config_path = GENERATED_REPO_DIR / "flexlate.json"
            config = FlexlateConfig.load(config_path)
            source = config.template_sources[0]
            source.target_version = template_source.version_2
            config.save()
            stage_and_commit_all(
                repo, f"Update target version for {template_source.name} to version 2"
            )

            # Make changes to update local templates to new version (no-op for remote templates)
            template_source.version_migrate_func(template_source.url_or_absolute_path)

            # Now update should go to new version
            fxt.update([template_source.name], no_input=True)

        assert_root_template_output_is_correct(
            template_source, after_version_update=True, after_data_update=True
        )
        # Other template unaffected
        assert_root_template_output_is_correct(
            non_update_template_source, template_source_index=1
        )

        project_config_path = GENERATED_REPO_DIR / "flexlate-project.json"
        _assert_project_config_is_correct(project_config_path, user=False)


@patch.object(appdirs, "user_config_dir", lambda name: GENERATED_FILES_DIR)
@pytest.mark.parametrize("user", [False, True])
def test_remove_template_source(

68fbff435e2f6257234d227b0bc400dd9dfa5f5f

Manual Update to Files from Copier Needed

The template from the Copier that created this project must be updated using Flexlate.

Normally this is an automated process, but there were merge conflicts while applying the update.

Run pipenv run fxt update -n then manually review and update the changes, before pushing a PR
for this.

πŸ“ Some templates are not up to date. Run fxt update to update
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Template Name ┃ Current Version ┃ Latest Version ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ copier-pypi-sphinx-fle… β”‚ 9fe07eb96b8702c40f1248… β”‚ c92b5c1f3fae3e9e9df04cb… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Allow updating template sources even when there are no applied templates

Allow updating template sources even when there are no applied templates

It should not be necessary to add this template for the update cli integration tests to pass

Need to rework the process of getting updates, right now it relies on there being applied templates

# TODO: Allow updating template sources even when there are no applied templates

    repo = repo_with_default_flexlate_project
    with change_directory_to(GENERATED_REPO_DIR):
        fxt.add_template_source(
            COPIER_REMOTE_URL, target_version=COPIER_REMOTE_VERSION_1
        )
        # TODO: Allow updating template sources even when there are no applied templates
        #  It should not be necessary to add this template for the update cli integration tests to pass
        #  Need to rework the process of getting updates, right now it relies on there being applied templates
        fxt.apply_template_and_add(COPIER_REMOTE_NAME, no_input=True)
    yield repo

0ce9c0fc9db299b878dadd2a09cc4232b20861fe

make sure it's the right git error

make sure it's the right git error

Could not fast forward. Must do a merge in a temp repo and have user resolve any conflicts

# TODO: make sure it's the right git error

from typing import Optional

from git import Repo, GitCommandError

from flexlate.branch_update import (
    get_flexlate_branch_name_for_feature_branch,
    abort_merge_and_reset_flexlate_branches,
)
from flexlate.cli_utils import confirm_user
from flexlate.constants import DEFAULT_MERGED_BRANCH_NAME, DEFAULT_TEMPLATE_BRANCH_NAME
from flexlate.ext_git import (
    fast_forward_branch_without_checkout,
    temp_repo_that_pushes_to_branch,
    repo_has_merge_conflicts,
    get_branch_sha,
    delete_local_branch,
)
from flexlate.styles import (
    print_styled,
    INFO_STYLE,
    ACTION_REQUIRED_STYLE,
    styled,
    QUESTION_STYLE,
    ALERT_STYLE,
    SUCCESS_STYLE,
)


class Merger:
    def merge_flexlate_branches(
        self,
        repo: Repo,
        branch_name: Optional[str] = None,
        delete: bool = True,
        merged_branch_name: str = DEFAULT_MERGED_BRANCH_NAME,
        template_branch_name: str = DEFAULT_TEMPLATE_BRANCH_NAME,
    ):
        feature_branch = branch_name or repo.active_branch.name
        flexlate_feature_merged_branch_name = (
            get_flexlate_branch_name_for_feature_branch(
                feature_branch, merged_branch_name
            )
        )
        flexlate_feature_template_branch_name = (
            get_flexlate_branch_name_for_feature_branch(
                feature_branch, template_branch_name
            )
        )

        # Save the status of the flexlate branches. We may need to roll back to this state
        # if the user aborts the merge
        current_branch = repo.active_branch
        merged_branch_sha = get_branch_sha(repo, merged_branch_name)
        template_branch_sha = get_branch_sha(repo, template_branch_name)

        print_styled(
            f"Merging {flexlate_feature_template_branch_name} to {template_branch_name}",
            INFO_STYLE,
        )
        try:
            fast_forward_branch_without_checkout(
                repo, template_branch_name, flexlate_feature_template_branch_name
            )
        except GitCommandError as e:
            # TODO: make sure it's the right git error
            # Could not fast forward. Must do a merge in a temp repo and have user resolve any conflicts
            with temp_repo_that_pushes_to_branch(  # type: ignore
                repo,
                branch_name=template_branch_name,
                base_branch_name=template_branch_name,
                additional_branches=(flexlate_feature_template_branch_name,),
            ) as temp_repo:
                temp_repo.git.merge(flexlate_feature_template_branch_name)  # type: ignore
                if repo_has_merge_conflicts(temp_repo):
                    print_styled(
                        f"Encountered merge conflicts while merging "
                        f"{flexlate_feature_template_branch_name} into {template_branch_name}",
                        INFO_STYLE,
                    )
                    print_styled(
                        f"Flexlate uses a temporary repo for this merge. "
                        f"Please resolve conflicts in {temp_repo.working_dir}",  # type: ignore
                        ACTION_REQUIRED_STYLE,
                    )
                    handled_conflicts = confirm_user(
                        styled(
                            "Successfully handled conflicts? n to abort", QUESTION_STYLE
                        )
                    )
                    if not handled_conflicts:
                        # Have only done work in a temp dir so far, so can just abort
                        print_styled("Aborting merge.", ALERT_STYLE)
                        return
        print_styled(
            f"Successfully merged {flexlate_feature_template_branch_name} to {template_branch_name}",
            SUCCESS_STYLE,
        )

        # Now merge the merged branch in the main repo
        try:
            fast_forward_branch_without_checkout(
                repo, merged_branch_name, flexlate_feature_merged_branch_name
            )
        except GitCommandError as e:
            # TODO: make sure it's the right git error
            # Could not fast forward. Must do a merge and have user resolve any conflicts
            merged_branch = repo.branches[merged_branch_name]  # type: ignore
            merged_branch.checkout()
            repo.git.merge(flexlate_feature_merged_branch_name)
            if repo_has_merge_conflicts(temp_repo):
                print_styled(
                    f"Encountered merge conflicts while merging "
                    f"{flexlate_feature_template_branch_name} into {template_branch_name}",
                    INFO_STYLE,
                )
                print_styled(f"Please resolve the conflicts", ACTION_REQUIRED_STYLE)
                handled_conflicts = confirm_user(
                    styled("Successfully handled conflicts? n to abort", QUESTION_STYLE)
                )
                if not handled_conflicts:
                    # Time ot abort, but need to reset the state of the branches
                    print_styled("Aborting merge.", ALERT_STYLE)
                    abort_merge_and_reset_flexlate_branches(
                        repo,
                        current_branch,
                        merged_branch_sha=merged_branch_sha,
                        template_branch_sha=template_branch_sha,
                        merged_branch_name=merged_branch_name,
                        template_branch_name=template_branch_name,
                    )

                    return

        print_styled(
            f"Successfully merged {flexlate_feature_merged_branch_name} to {merged_branch_name}",
            SUCCESS_STYLE,
        )

        if not delete:
            return

        # Handle delete
        print_styled(
            f"Deleting flexlate feature branches {flexlate_feature_template_branch_name} and {flexlate_feature_merged_branch_name}",
            INFO_STYLE,
        )
        delete_local_branch(repo, flexlate_feature_template_branch_name)
        delete_local_branch(repo, flexlate_feature_merged_branch_name)
        print_styled(f"Successfully deleted flexlate feature branches", SUCCESS_STYLE)

77dc5e6f4e6d40fdd1c051d54b94861c9e3225e5

can use get all renderables

can use get all renderables

# TODO: can use get all renderables

            raise ValueError("repo working dir should not be none")

        project_root = Path(repo.working_dir)
        template = config_manager.get_template_by_name(
            template_name, project_root=project_root
        )

        # Determine location of config
        # Need to get renderable to render path in case it is templated
        # TODO: can use get all renderables
        renderables = config_manager.get_renderables_for_updates(
            config_manager.get_no_op_updates(project_root=project_root),
            project_root=project_root,
        )
        if len(renderables) == 0:
            raise CannotRemoveAppliedTemplateException(
                f"Cannot find any applied template with name {template_name} "
                f"because there are no applied templates"
            )
        renderable = renderables[0]
        new_relative_out_root = Path(
            renderer.render_string(
                str(template.render_relative_root_in_output), renderable
            )
        )
        full_local_config_out_root = out_root / new_relative_out_root
        config_path = determine_config_path_from_roots_and_add_mode(
            full_local_config_out_root, project_root, add_mode
        )

        expanded_out_root = get_expanded_out_root(
            out_root, project_root, template.render_relative_root_in_output, add_mode
        )

        if add_mode == AddMode.USER:
            # No need to commit config changes for user

e73e5fa7646353a64f7778a5bca6696392d679cd

Manual Update to Files from Copier Needed

The template from the Copier that created this project must be updated using Flexlate.

Normally this is an automated process, but there were merge conflicts while applying the update.

Run pipenv run fxt update -n then manually review and update the changes, before pushing a PR
for this.

πŸ“ Some templates are not up to date. Run fxt update to update
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Template Name ┃ Current Version ┃ Latest Version ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ copier-pypi-sphinx-fle… β”‚ c92b5c1f3fae3e9e9df04c… β”‚ 26fe30f1a62c2526566d060… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Manual Update to Files from Copier Needed

The template from the Copier that created this project must be updated using Flexlate.

Normally this is an automated process, but there were merge conflicts while applying the update.

Run pipenv run fxt update -n then manually review and update the changes, before pushing a PR
for this.

πŸ“ Some templates are not up to date. Run fxt update to update
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Template Name ┃ Current Version ┃ Latest Version ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ copier-pypi-sphinx-fle… β”‚ c92b5c1f3fae3e9e9df04c… β”‚ 26fe30f1a62c2526566d060… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Manual Update to Files from Copier Needed

The template from the Copier that created this project must be updated using Flexlate.

Normally this is an automated process, but there were merge conflicts while applying the update.

Run pipenv run fxt update -n then manually review and update the changes, before pushing a PR
for this.

πŸ“ Some templates are not up to date. Run fxt update to update
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Template Name ┃ Current Version ┃ Latest Version ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ copier-pypi-sphinx-fle… β”‚ 9fe07eb96b8702c40f1248… β”‚ 89070159fd04c9efb3ccfb8… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Improve error message for can't remove template source

Improve error message for can't remove template source

When can't remove template source due to existing applied template, determine paths where

the existing applied templates are to inform the user what needs to be removed

# TODO: Improve error message for can't remove template source

        child_config.template_sources.append(source)
        self.save_config(config)

    def remove_template_source(
        self,
        template_name: str,
        config_path: Path,
        project_root: Path = Path("."),
    ):
        config = self.load_config(project_root=project_root)
        if self._applied_template_exists_in_project(
            template_name, project_root=project_root, config=config
        ):
            # TODO: Improve error message for can't remove template source
            #  When can't remove template source due to existing applied template, determine paths where
            #  the existing applied templates are to inform the user what needs to be removed
            raise CannotRemoveTemplateSourceException(
                f"Cannot remove template source {template_name} as it has existing outputs"
            )

        child_config = _get_or_create_child_config_by_path(config, config_path)
        template_index: Optional[int] = None
        for i, template_source in enumerate(child_config.template_sources):
            if template_source.name == template_name:
                template_index = i
                break
        if template_index is None:
            raise CannotRemoveTemplateSourceException(
                f"Cannot find any template source with name {template_name}"
            )
        child_config.template_sources.pop(template_index)
        self.save_config(config)

    def _applied_template_exists_in_project(
        self,
        template_name: str,
        project_root: Path = Path("."),
        config: Optional[FlexlateConfig] = None,
    ):
        config = config or self.load_config(project_root=project_root)
        for child_config in config.child_configs:
            for applied_template in child_config.applied_templates:
                if applied_template.name == template_name:
                    return True
        return False

    def add_applied_template(
        self,
        template: Template,

a5a9279a34e1366e842b6f6f659f779701dde9c4

is there a way to set up remote tracking branches without checkout?

is there a way to set up remote tracking branches without checkout?

# TODO: is there a way to set up remote tracking branches without checkout?

    if not _branch_exists(temp_repo, branch_name):
        # Now create the new branch
        checkout_template_branch(temp_repo, branch_name, base_branch_name)

    return temp_repo


def _clone_specific_branches_from_local_repo(
    repo: Repo,
    out_dir: Path,
    branch_names: Sequence[str],
    checkout_branch: str,
    base_checkout_branch: str,
) -> Repo:
    temp_repo = Repo.init(out_dir)
    valid_branches = [name for name in branch_names if _branch_exists(repo, name)]
    branch_arguments = list(
        itertools.chain(*[["-t", branch] for branch in valid_branches])
    )
    temp_repo.git.remote("add", "-f", *branch_arguments, "origin", repo.working_dir)
    for branch in valid_branches:
        # TODO: is there a way to set up remote tracking branches without checkout?
        temp_repo.git.checkout("-t", f"origin/{branch}")

    if _branch_exists(repo, checkout_branch):
        temp_repo.branches[checkout_branch].checkout()  # type: ignore
    else:
        # Create the branch
        checkout_template_branch(repo, checkout_branch, base_checkout_branch)
    return temp_repo

8434ee735d2510c4cfdf85771f3fef52a88d20c4

Manual Update to Files from Copier Needed

The template from the Copier that created this project must be updated using Flexlate.

Normally this is an automated process, but there were merge conflicts while applying the update.

Run pipenv run fxt update -n then manually review and update the changes, before pushing a PR
for this.

πŸ“ Some templates are not up to date. Run fxt update to update
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Template Name ┃ Current Version ┃ Latest Version ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ copier-pypi-sphinx-fle… β”‚ 9fe07eb96b8702c40f1248… β”‚ 89070159fd04c9efb3ccfb8… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

throw an error when user tries to target version for a local template

throw an error when user tries to target version for a local template

# TODO: throw an error when user tries to target version for a local template

                ),
                TemplateSource.from_template(
                    # It is actually useless to put target version in a local template
                    # TODO: throw an error when user tries to target version for a local template
                    local_template,
                    target_version=COOKIECUTTER_ONE_VERSION,
                ),

ef2b9cb0d0f20247a8aba07ade19032c0362f8c2

Manual Update to Files from Copier Needed

The template from the Copier that created this project must be updated using Flexlate.

Normally this is an automated process, but there were merge conflicts while applying the update.

Run pipenv run fxt update -n then manually review and update the changes, before pushing a PR
for this.

πŸ“ Some templates are not up to date. Run fxt update to update
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Template Name ┃ Current Version ┃ Latest Version ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ copier-pypi-sphinx-fle… β”‚ 9fe07eb96b8702c40f1248… β”‚ 89070159fd04c9efb3ccfb8… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

replace with cli command to update target version once it exists

replace with cli command to update target version once it exists

# TODO: replace with cli command to update target version once it exists

import json

import pytest
from git import Repo

from flexlate.config import FlexlateConfig
from flexlate.ext_git import stage_and_commit_all
from flexlate.main import Flexlate
from flexlate.path_ops import change_directory_to
from tests.config import (
    GENERATED_REPO_DIR,
    COOKIECUTTER_REMOTE_URL,
    COOKIECUTTER_REMOTE_VERSION_1,
)
from tests.fixtures.git import *

fxt = Flexlate()


@pytest.fixture
def repo_with_default_flexlate_project(repo_with_placeholder_committed: Repo) -> Repo:
    repo = repo_with_placeholder_committed
    with change_directory_to(GENERATED_REPO_DIR):
        fxt.init_project()
    yield repo


@pytest.fixture
def repo_with_copier_remote_version_one(
    repo_with_default_flexlate_project: Repo,
) -> Repo:
    repo = repo_with_default_flexlate_project
    with change_directory_to(GENERATED_REPO_DIR):
        fxt.add_template_source(
            COOKIECUTTER_REMOTE_URL, target_version=COOKIECUTTER_REMOTE_VERSION_1
        )
    yield repo


@pytest.fixture
def repo_with_copier_remote_version_one_and_no_target_version(
    repo_with_copier_remote_version_one: Repo,
) -> Repo:
    repo = repo_with_copier_remote_version_one
    # TODO: replace with cli command to update target version once it exists
    config_path = GENERATED_REPO_DIR / "flexlate.json"
    config = FlexlateConfig.load(config_path)
    source = config.template_sources[0]
    source.target_version = None
    config.save()
    stage_and_commit_all(repo, f"Remove target version for copier remote")
    yield repo


@pytest.fixture
def repo_with_copier_remote_version_one_no_target_version_and_will_have_a_conflict_on_update(
    repo_with_copier_remote_version_one_and_no_target_version: Repo,
) -> Repo:
    repo = repo_with_copier_remote_version_one_and_no_target_version
    # Reformat the flexlate config to cause a conflict
    config_path = GENERATED_REPO_DIR / "flexlate.json"
    config_data = json.loads(config_path.read_text())
    config_path.write_text(json.dumps(config_data, indent=4))
    stage_and_commit_all(repo, "Reformat flexlate config to cause a conflict on update")
    yield repo

184b0f7b5b6f97ffd613bbc178189e4b2c0b002f

Create new templates rather than updating existing ones

Create new templates rather than updating existing ones

This would eliminate issues from partial updates of templates as new attributes are added.

I think the logic is set up this way because order of the templates matters.

As finder already updates the local files, just update the template object

# TODO: Create new templates rather than updating existing ones

                kwargs.update(version=source.target_version)
            for template in source_with_templates.templates:
                new_template = finder.find(str(source.update_location), **kwargs)
                # TODO: Create new templates rather than updating existing ones
                #  This would eliminate issues from partial updates of templates as new attributes are added.
                #  I think the logic is set up this way because order of the templates matters.
                if template.version != new_template.version:
                    # Template needs to be upgraded
                    # As finder already updates the local files, just update the template object
                    template.update_from_template(new_template)


def _commit_message(renderables: Sequence[Renderable]) -> str:

6505e6c71412bf9ae45eb60150ffa0de9f634c13

Update target versions in template sources

Update target versions in template sources

# TODO: Update target versions in template sources

        )

    # TODO: list template sources, list applied templates, remove applied templates and sources
    # TODO: Update target versions in template sources

3ab52b1cdd7a4745fbcc18ca635091f0b1a7e674

Can we remove target_version from templates?

Can we remove target_version from templates?

# TODO: Can we remove target_version from templates?

                f"no handling for template type {self.type} in creating template from source"
            )
        kwargs = dict(name=self.name)
        version = version or self.version
        if version is not None:
            kwargs.update(version=version)
        # TODO: Can we remove target_version from templates?
        if self.target_version is not None:
            kwargs.update(target_version=self.target_version)
        if self.git_url is not None:

2d125c5846979bc328a409ee11bdcb283fdbf370

Manual Update to Files from Copier Needed

The template from the Copier that created this project must be updated using Flexlate.

Normally this is an automated process, but there were merge conflicts while applying the update.

Run pipenv run fxt update -n then manually review and update the changes, before pushing a PR
for this.

πŸ“ Some templates are not up to date. Run fxt update to update
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Template Name ┃ Current Version ┃ Latest Version ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ copier-pypi-sphinx-fle… β”‚ 89070159fd04c9efb3ccfb… β”‚ c92b5c1f3fae3e9e9df04cb… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

more efficient algorithm for matching rendered data with updates

more efficient algorithm for matching rendered data with updates

# TODO: more efficient algorithm for matching rendered data with updates

    class Config:
        arbitrary_types_allowed = True

    def to_applied_template(
        self, project_root: Path = Path("."), adjust_root: bool = True
    ) -> AppliedTemplateConfig:
        config = FlexlateConfig.load(self.config_location)
        applied_template = config.applied_templates[self.index]
        if applied_template.name != self.template.name:
            raise CannotFindAppliedTemplateException(
                f"could not find applied template for name {self.template.name} "
                f"and index {self.index} in config {self.config_location}"
            )
        # Update the data if it is supplied in the update
        if self.data:
            applied_template.data = self.data
        if adjust_root:
            # Because this config may not be at the project root, adjust the path as if it was
            absolute_orig_root = (
                applied_template.root
                if applied_template.root.is_absolute()
                else (
                    self.config_location.parent.resolve() / applied_template.root
                ).resolve()
            )
            new_root = Path(os.path.relpath(absolute_orig_root, project_root))
            applied_template.root = new_root
        return applied_template

    def to_renderable(
        self, project_root: Path = Path("."), adjust_root: bool = True
    ) -> Renderable[Template]:
        applied_template = self.to_applied_template(
            project_root=project_root, adjust_root=adjust_root
        )
        renderable: Renderable[Template] = Renderable(
            template=self.template,
            data=applied_template.data,
            out_root=applied_template.root,
        )
        return renderable

    def matches_renderable(
        self,
        renderable: Renderable,
        project_root: Path = Path("."),
        render_root: Path = Path("."),
        adjust_root: bool = True,
    ) -> bool:
        """
        Note: Does not check template data
        """
        self_renderable = self.to_renderable(
            project_root=project_root, adjust_root=adjust_root
        )
        self_renderable_out_root: Path = self_renderable.out_root
        if not self_renderable_out_root.is_absolute():
            self_renderable_out_root = (
                render_root / self_renderable_out_root
            ).resolve()
        renderable_out_root: Path = renderable.out_root
        if not renderable_out_root.is_absolute():
            renderable_out_root = (render_root / renderable_out_root).resolve()
        return all(
            [
                self_renderable.template.name == renderable.template.name,
                self_renderable.template.version == renderable.template.version,
                self_renderable_out_root == renderable_out_root,
            ]
        )


def data_from_template_updates(updates: Sequence[TemplateUpdate]) -> List[TemplateData]:
    return [update.data or {} for update in updates]


def updates_with_updated_data(
    updates: Sequence[TemplateUpdate],
    data: Sequence[TemplateData],
    renderables: Sequence[Renderable],
    project_root: Path = Path("."),
    render_root: Path = Path("."),
    adjust_root: bool = True,
) -> List[TemplateUpdate]:
    if len(data) != len(renderables):
        raise InvalidTemplateDataException(
            f"should have equal length data {data} and renderables {renderables}"
        )
    out_updates: List[TemplateUpdate] = []
    for update in updates:
        new_update = deepcopy(update)
        # TODO: more efficient algorithm for matching rendered data with updates
        this_update_data: Optional[TemplateData] = None
        for i, renderable in enumerate(renderables):
            if update.matches_renderable(
                renderable,
                project_root=project_root,
                render_root=render_root,
                adjust_root=adjust_root,
            ):
                this_update_data = data[i]
                break
        if this_update_data is None:
            raise InvalidTemplateDataException(
                f"Could not find matching renderable for update {update} with "
                f"renderables {renderables}. Therefore data could not be matched"
            )
        new_update.data = this_update_data
        out_updates.append(new_update)
    return out_updates

0dfb10f1858e47813fa214c55c2fbf780602eeae

more efficient algorithm for getting applied templates with sources

more efficient algorithm for getting applied templates with sources

# TODO: more efficient algorithm for getting applied templates with sources

        config: Optional[FlexlateConfig] = None,
    ) -> List[AppliedTemplateWithSource]:
        config = config or self.load_config(project_root)

        # TODO: more efficient algorithm for getting applied templates with sources

        def get_source_config_path(source_name: str) -> Path:
            if config is None:
                raise ValueError("should not hit this, for type narrowing")
            for child_config in config.child_configs:
                for source in child_config.template_sources:
                    if source.name == source_name:
                        return child_config.settings.config_location
            raise ValueError(f"could not find source {source_name}")

        sources = config.template_sources_dict
        applied_template_with_sources: List[AppliedTemplateWithSource] = []

        for child_config in config.child_configs:
            for i, applied_template in enumerate(child_config.applied_templates):
                source = sources[applied_template.name]
                source_config_path = get_source_config_path(source.name)
                applied_template_config_path = child_config.settings.config_location
                if (
                    relative_to is not None
                    and source.git_url is None
                    and not Path(source.path).is_absolute()
                ):
                    new_path = (relative_to / Path(source.path)).resolve()
                    use_source = source.copy(update=dict(path=new_path))
                else:
                    use_source = source
                applied_template_with_sources.append(
                    AppliedTemplateWithSource(
                        applied_template=applied_template,
                        source=use_source,
                        index=i,
                        source_config_path=source_config_path,
                        applied_template_config_path=applied_template_config_path,
                    )
                )
        return applied_template_with_sources

    def get_all_renderables(

07b2332c4e8c75086847d35f9414aca5441cdd80

Manual Update to Files from Copier Needed

The template from the Copier that created this project must be updated using Flexlate.

Normally this is an automated process, but there were merge conflicts while applying the update.

Run pipenv run fxt update -n then manually review and update the changes, before pushing a PR
for this.

πŸ“ Some templates are not up to date. Run fxt update to update
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Template Name ┃ Current Version ┃ Latest Version ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ copier-pypi-sphinx-fle… β”‚ b0916a7b0b576ebc6441d1… β”‚ 68fa0b20d416ea791a46bf1… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Manual Update to Files from Copier Needed

The template from the Copier that created this project must be updated using Flexlate.

Normally this is an automated process, but there were merge conflicts while applying the update.

Run pipenv run fxt update -n then manually review and update the changes, before pushing a PR
for this.

πŸ“ Some templates are not up to date. Run fxt update to update
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Template Name ┃ Current Version ┃ Latest Version ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
β”‚ copier-pypi-sphinx-fle… β”‚ b0916a7b0b576ebc6441d1… β”‚ 68fa0b20d416ea791a46bf1… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

add AddMode.USER to tests by creating user config fixture

add AddMode.USER to tests by creating user config fixture

# TODO: add AddMode.USER to tests by creating user config fixture

    assert source.target_version == "some version"


# TODO: add AddMode.USER to tests by creating user config fixture


@patch.object(appdirs, "user_config_dir", lambda name: GENERATED_FILES_DIR)
@pytest.mark.parametrize("add_mode", [AddMode.LOCAL, AddMode.PROJECT])
def test_add_local_cookiecutter_applied_template_to_repo(
    add_mode: AddMode,
    repo_with_cookiecutter_one_template_source: Repo,
    cookiecutter_one_template: CookiecutterTemplate,
):

8dab9b555f3c206442514817b8e2904221beaca4

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.