├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── CMakeLists.txt ├── Dockerfile.uclibc ├── README.md ├── _clang-format ├── build-uclibc.sh ├── cli ├── CMakeLists.txt ├── commands │ ├── add_command.cpp │ ├── add_command.h │ ├── base_command.h │ ├── down_command.cpp │ ├── down_command.h │ ├── list_command.cpp │ ├── list_command.h │ ├── remove_command.cpp │ ├── remove_command.h │ ├── up_command.cpp │ └── up_command.h ├── compose_executor.cpp ├── compose_executor.h ├── config.h.in ├── dcw_config.cpp ├── dcw_config.h ├── main.cpp ├── process_executor.cpp ├── process_executor.h ├── state_repository.cpp ├── state_repository.h ├── workspace.h ├── workspace_service.cpp ├── workspace_service.h ├── workspaces_repository.h ├── yaml_config.cpp ├── yaml_config.h ├── yaml_workspaces_repository.cpp └── yaml_workspaces_repository.h ├── completion.bash ├── conanfile.txt ├── install.sh ├── reformat_sources.sh └── update_changelog_from_git.sh /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build dcw 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v4 9 | 10 | - name: Build 11 | run: ./build-uclibc.sh 12 | 13 | - name: Upload Artifact 14 | uses: actions/upload-artifact@v4 15 | with: 16 | name: dcw 17 | path: dist/dcw 18 | 19 | - name: Release 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 22 | uses: softprops/action-gh-release@v1 23 | if: startsWith(github.ref, 'refs/tags/') 24 | with: 25 | files: dist/dcw 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /CMakeLists.txt.user 3 | /CMakeUserPresets.json 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.3.1 (2025-05-10) 2 | 3 | - Fixed hang on project file detection 4 | 5 | # 1.3.0 (2025-02-16) 6 | 7 | - Add support for project file `.dcw` 8 | 9 | # 1.2.3 (2024-12-20) 10 | 11 | - Pack binary with UPX 12 | 13 | # 1.2.2 (2024-10-31) 14 | 15 | - Fix removing of current workspace if compose file does not exists 16 | 17 | # 1.2.1 (2024-09-17) 18 | 19 | - Add `--build` parameter to compose up command 20 | 21 | # 1.2.0 (2024-05-08) 22 | 23 | - Switch to new command line parser [CLI11](https://github.com/CLIUtils/CLI11) 24 | - Suggest workspace name in `up` command if it not provided. Name suggested if current directory matches workspace or 25 | try to up the latest active workspace. 26 | - Added shortcuts for all commands 27 | - `dcw` without args shows a list of all workspaces 28 | 29 | # 1.1.2 (2024-04-24) 30 | 31 | - Fix location of state yaml file to `~/.local/share/dcw/state.yml` 32 | 33 | # 1.1.1 (2024-04-09) 34 | 35 | - Fix work dir when running `docker compose` 36 | 37 | # 1.1.0 (2024-04-05) 38 | 39 | - Add bash completion 40 | - Add installation script 41 | - Support for `docker compose` command 42 | - Terminate partially started containers if the entire workspace cannot be started 43 | 44 | # 1.0.0 (2024-04-04) 45 | 46 | - Initial release 47 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project(dcw LANGUAGES CXX) 4 | 5 | set(CMAKE_CXX_STANDARD 20) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | 8 | string(TOUPPER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_UPPERCASE) 9 | 10 | if(CMAKE_BUILD_TYPE_UPPERCASE MATCHES "(RELEASE|RELWITHDEBINFO|MINSIZEREL)") 11 | set(RELEASE_MODE ON) 12 | endif() 13 | 14 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic -Werror -Wno-comment -Wno-reorder -Werror=return-type -Woverloaded-virtual -Wno-unused-parameter") 15 | 16 | if(NOT RELEASE_MODE) 17 | # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic -Werror -Wno-comment -Wno-reorder -Werror=return-type -Woverloaded-virtual") 18 | else() 19 | endif() 20 | 21 | # read version 22 | file(READ "CHANGELOG.md" VERSION_FILE_CONTENT) 23 | string(REGEX REPLACE "^# ([0-9.]+).*$" "\\1" APP_VERSION ${VERSION_FILE_CONTENT}) 24 | message(STATUS "App version: " ${APP_VERSION}) 25 | 26 | # patch files with version 27 | function(patch_version FILE REGEXP) 28 | file(READ ${FILE} FILE_CONTENT) 29 | string(REGEX REPLACE ${REGEXP} "\\1${APP_VERSION}\\2" NEW_FILE_CONTENT ${FILE_CONTENT}) 30 | file(WRITE ${FILE} ${NEW_FILE_CONTENT}) 31 | endfunction() 32 | 33 | patch_version("install.sh" "(releases/download/)[0-9.]+(/dcw)") 34 | 35 | add_subdirectory(cli) 36 | -------------------------------------------------------------------------------- /Dockerfile.uclibc: -------------------------------------------------------------------------------- 1 | FROM navrocky/buildroot-uclibc-toolchain:i686-2023.11.1 2 | 3 | RUN set -x && \ 4 | apt-get update && \ 5 | apt-get -y --no-install-recommends install cmake make python3-pip ninja-build curl libmpc3 && \ 6 | pip install conan --break-system-packages 7 | 8 | ENV UPX_VER=4.2.4 9 | RUN set -x && \ 10 | curl -LO https://github.com/upx/upx/releases/download/v${UPX_VER}/upx-${UPX_VER}-amd64_linux.tar.xz && \ 11 | xzdec upx-${UPX_VER}-amd64_linux.tar.xz | tar xf - && \ 12 | install upx-${UPX_VER}-amd64_linux/upx /usr/bin/upx && \ 13 | rm -rf upx-${UPX_VER}-amd64_linux upx-${UPX_VER}-amd64_linux.tar.xz 14 | 15 | RUN conan profile detect --force && sed -i 's/x86_64/x86/g' ~/.conan2/profiles/default 16 | 17 | COPY conanfile.txt /sources/ 18 | 19 | RUN set -x && \ 20 | mkdir -p /build && \ 21 | cd /build && \ 22 | conan install /sources --output-folder=. --build=missing 23 | 24 | COPY . /sources/ 25 | 26 | RUN set -x && \ 27 | cd /build && \ 28 | cmake /sources -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release -G Ninja && \ 29 | cmake --build . && \ 30 | upx -9 cli/dcw 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Compose Workspace manager 2 | 3 | It helps to manage developer test environments based on docker-compose files. 4 | 5 | ## Installation 6 | 7 | The released binaries are statically compiled, so they can run in almost all linux distributions. App running is tested in Ubuntu 4 (2006). 8 | Supported platforms is: 9 | 10 | * Linux x86, x86_64 11 | 12 | To install `dcw` system wide with bash completion run this command in your terminal: 13 | 14 | ```sh 15 | sudo sh -c 'curl -sSL https://github.com/navrocky/dcw/raw/master/install.sh | bash' 16 | ``` 17 | 18 | ## Usage 19 | 20 | ``` 21 | Usage: dcw [OPTIONS] SUBCOMMAND 22 | 23 | Options: 24 | -h,--help Print this help message and exit 25 | 26 | Subcommands: 27 | add, a Add named docker compose file as workspace 28 | rm, r Remove workspace 29 | list, l List registered workspaces 30 | up, u Switch to the workspace 31 | down, d Down current workspace 32 | ``` 33 | 34 | ## Project file 35 | 36 | `dcw` can use the `.dcw` project file in the source root to get information about a new project. 37 | 38 | Just run `dcw up` in the root of the project with the `.dcw` file and let `dcw` take care of the rest. 39 | 40 | Sample `.dcw` project file: 41 | 42 | ``` 43 | name: myproj 44 | composeFile: env/docker-compose.yml 45 | ``` 46 | 47 | A project file can be automatically created with the `add` command with the `-p, --create-project` flag. 48 | 49 | ## Examples 50 | 51 | ### Add workspace: 52 | 53 | ```sh 54 | dcw add myproj /home/user/myproj/docker-compose.yml 55 | ``` 56 | 57 | ### Add workspace and create dcw project file: 58 | 59 | ```sh 60 | dcw add -p myproj docker-compose.yml 61 | ``` 62 | 63 | ### List workspaces and view active: 64 | 65 | ```sh 66 | dcw list 67 | ``` 68 | 69 | Output is: 70 | ``` 71 | 🟢 myproj: /home/user/myproj/docker-compose.yml 72 | ⚫ otherproj: /home/user/otherproj/docker-compose.yml 73 | ``` 74 | 75 | ### Switch to workspace 76 | 77 | ```sh 78 | dcw up otherproj 79 | ``` 80 | 81 | ### Stop workspace and optionally purge workspace data (volumes) 82 | 83 | ```sh 84 | dcw down -p 85 | ``` 86 | -------------------------------------------------------------------------------- /_clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: WebKit 3 | --- 4 | Language: Cpp 5 | NamespaceIndentation: None 6 | ColumnLimit: 120 7 | AllowShortEnumsOnASingleLine: false 8 | IndentCaseLabels: true 9 | AlwaysBreakTemplateDeclarations: true 10 | BraceWrapping: 11 | AfterEnum: false 12 | ... 13 | -------------------------------------------------------------------------------- /build-uclibc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | IMAGE_NAME=dcw-build 4 | 5 | docker build --progress=plain --tag ${IMAGE_NAME} -f Dockerfile.uclibc . 6 | 7 | id=$(docker create ${IMAGE_NAME}) 8 | mkdir -p dist 9 | docker cp $id:/build/cli/dcw ./dist/ 10 | docker rm -v $id 11 | -------------------------------------------------------------------------------- /cli/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(TARGET dcw) 2 | 3 | find_package(termcolor REQUIRED) 4 | find_package(yaml-cpp REQUIRED) 5 | find_package(CLI11 REQUIRED) 6 | 7 | add_link_options(-static -static-libgcc -static-libstdc++) 8 | 9 | include_directories(${CMAKE_CURRENT_BINARY_DIR}) 10 | 11 | if(RELEASE_MODE) 12 | add_link_options(-s -Os) 13 | endif() 14 | 15 | configure_file(config.h.in config.h) 16 | 17 | set(SOURCES 18 | main.cpp 19 | yaml_config.h 20 | yaml_config.cpp 21 | workspaces_repository.h 22 | workspace_service.h 23 | workspace_service.cpp 24 | state_repository.h 25 | state_repository.cpp 26 | process_executor.h 27 | process_executor.cpp 28 | compose_executor.h 29 | compose_executor.cpp 30 | workspace.h 31 | dcw_config.h 32 | dcw_config.cpp 33 | 34 | yaml_workspaces_repository.h 35 | yaml_workspaces_repository.cpp 36 | commands/add_command.h 37 | commands/add_command.cpp 38 | commands/remove_command.h 39 | commands/remove_command.cpp 40 | commands/base_command.h 41 | commands/list_command.h 42 | commands/list_command.cpp 43 | commands/up_command.h 44 | commands/up_command.cpp 45 | commands/down_command.h 46 | commands/down_command.cpp 47 | ) 48 | 49 | add_executable(${TARGET} ${SOURCES}) 50 | 51 | target_link_libraries(${TARGET} 52 | termcolor::termcolor 53 | yaml-cpp::yaml-cpp 54 | CLI11::CLI11 55 | ) 56 | 57 | include(GNUInstallDirs) 58 | install(TARGETS ${TARGET} 59 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 60 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 61 | ) 62 | -------------------------------------------------------------------------------- /cli/commands/add_command.cpp: -------------------------------------------------------------------------------- 1 | #include "add_command.h" 2 | 3 | #include 4 | 5 | using namespace std; 6 | 7 | AddCommand::AddCommand(const WorkspaceServicePtr& service) 8 | : service(service) 9 | { 10 | } 11 | 12 | void AddCommand::reg(CLI::App& app) 13 | { 14 | auto cmd = app.add_subcommand("add", "Add named docker compose file as workspace") 15 | ->alias("a") 16 | ->callback(std::bind(&AddCommand::process, this)); 17 | cmd->add_option("name", name, "Workspace name")->required(); 18 | cmd->add_option("file", file, "Docker compose file")->required(); 19 | cmd->add_flag("-p, --create-project", createProjectFile, "Create project file in current directory"); 20 | } 21 | 22 | void AddCommand::process() 23 | { 24 | auto composeFilePath = std::filesystem::absolute(file); 25 | service->add(name, composeFilePath, createProjectFile); 26 | } 27 | -------------------------------------------------------------------------------- /cli/commands/add_command.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../workspace_service.h" 4 | #include "base_command.h" 5 | 6 | class AddCommand : public BaseCommand { 7 | public: 8 | AddCommand(const WorkspaceServicePtr& service); 9 | 10 | void reg(CLI::App& app) override; 11 | 12 | private: 13 | void process(); 14 | 15 | WorkspaceServicePtr service; 16 | std::string name; 17 | std::string file; 18 | bool createProjectFile; 19 | }; 20 | -------------------------------------------------------------------------------- /cli/commands/base_command.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class BaseCommand { 7 | public: 8 | virtual ~BaseCommand() { } 9 | virtual void reg(CLI::App& app) = 0; 10 | }; 11 | 12 | using CommandPtr = std::shared_ptr; 13 | -------------------------------------------------------------------------------- /cli/commands/down_command.cpp: -------------------------------------------------------------------------------- 1 | #include "down_command.h" 2 | 3 | DownCommand::DownCommand(const WorkspaceServicePtr& service) 4 | : service(service) 5 | , purge(false) 6 | { 7 | } 8 | 9 | void DownCommand::reg(CLI::App& app) 10 | { 11 | auto cmd = app.add_subcommand("down", "Down current workspace") 12 | ->alias("d") 13 | ->callback(std::bind(&DownCommand::process, this)); 14 | cmd->add_flag("-p, --purge", purge, "Purge workspace data (docker volumes)"); 15 | } 16 | 17 | void DownCommand::process() { service->down(purge); } 18 | -------------------------------------------------------------------------------- /cli/commands/down_command.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../workspace_service.h" 4 | #include "base_command.h" 5 | 6 | class DownCommand : public BaseCommand { 7 | public: 8 | DownCommand(const WorkspaceServicePtr& service); 9 | 10 | void reg(CLI::App& app) override; 11 | 12 | private: 13 | void process(); 14 | WorkspaceServicePtr service; 15 | bool purge; 16 | }; 17 | -------------------------------------------------------------------------------- /cli/commands/list_command.cpp: -------------------------------------------------------------------------------- 1 | #include "list_command.h" 2 | 3 | ListCommand::ListCommand(const WorkspaceServicePtr& service) 4 | : service(service) 5 | , namesOnly(false) 6 | { 7 | } 8 | 9 | void ListCommand::reg(CLI::App& app) 10 | { 11 | auto cmd = app.add_subcommand("list", "List registered workspaces") 12 | ->alias("l") 13 | ->callback(std::bind(&ListCommand::process, this)); 14 | cmd->add_flag("-n, --names", namesOnly, "Show names only"); 15 | } 16 | 17 | void ListCommand::process() { service->list(namesOnly); } 18 | -------------------------------------------------------------------------------- /cli/commands/list_command.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../workspace_service.h" 4 | #include "base_command.h" 5 | 6 | class ListCommand : public BaseCommand { 7 | public: 8 | ListCommand(const WorkspaceServicePtr& service); 9 | 10 | void reg(CLI::App& app) override; 11 | 12 | private: 13 | void process(); 14 | WorkspaceServicePtr service; 15 | bool namesOnly; 16 | }; 17 | -------------------------------------------------------------------------------- /cli/commands/remove_command.cpp: -------------------------------------------------------------------------------- 1 | #include "remove_command.h" 2 | 3 | RemoveCommand::RemoveCommand(const WorkspaceServicePtr& service) 4 | : service(service) 5 | { 6 | } 7 | 8 | void RemoveCommand::reg(CLI::App& app) 9 | { 10 | auto cmd 11 | = app.add_subcommand("rm", "Remove workspace")->alias("r")->callback(std::bind(&RemoveCommand::process, this)); 12 | cmd->add_option("name", name, "Name of the workspace"); 13 | } 14 | 15 | void RemoveCommand::process() { service->remove(name); } 16 | -------------------------------------------------------------------------------- /cli/commands/remove_command.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../workspace_service.h" 4 | #include "base_command.h" 5 | 6 | class RemoveCommand : public BaseCommand { 7 | public: 8 | RemoveCommand(const WorkspaceServicePtr& service); 9 | 10 | void reg(CLI::App& app) override; 11 | 12 | private: 13 | void process(); 14 | WorkspaceServicePtr service; 15 | std::string name; 16 | }; 17 | -------------------------------------------------------------------------------- /cli/commands/up_command.cpp: -------------------------------------------------------------------------------- 1 | #include "up_command.h" 2 | 3 | UpCommand::UpCommand(const WorkspaceServicePtr& service) 4 | : service(service) 5 | , clean(false) 6 | { 7 | } 8 | 9 | void UpCommand::reg(CLI::App& app) 10 | { 11 | auto cmd = app.add_subcommand("up", "Switch to the workspace") 12 | ->alias("u") 13 | ->callback(std::bind(&UpCommand::process, this)); 14 | cmd->add_option("name", name, "Workspace name"); 15 | cmd->add_flag("-c, --clean", clean, "Purge workspace data (docker volumes) before start"); 16 | } 17 | 18 | void UpCommand::process() { service->up(name, clean); } 19 | -------------------------------------------------------------------------------- /cli/commands/up_command.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../workspace_service.h" 4 | #include "base_command.h" 5 | 6 | class UpCommand : public BaseCommand { 7 | public: 8 | UpCommand(const WorkspaceServicePtr& service); 9 | 10 | void reg(CLI::App& app) override; 11 | 12 | private: 13 | void process(); 14 | WorkspaceServicePtr service; 15 | std::string name; 16 | bool clean; 17 | }; 18 | -------------------------------------------------------------------------------- /cli/compose_executor.cpp: -------------------------------------------------------------------------------- 1 | #include "compose_executor.h" 2 | 3 | #include 4 | #include 5 | 6 | using namespace std; 7 | 8 | void checkFileExists(const std::string& file) 9 | { 10 | if (!filesystem::exists(file)) 11 | throw runtime_error(format("File \"{}\" not exists", file)); 12 | } 13 | 14 | ComposeExecutorImpl::ComposeExecutorImpl(ProcessExecutorPtr processExecutor) 15 | : processExecutor(processExecutor) 16 | { 17 | if (processExecutor->exec("docker compose --help > /dev/null") == 0) 18 | composeCommand = "docker compose"; 19 | else 20 | composeCommand = "docker-compose"; 21 | } 22 | 23 | void ComposeExecutorImpl::up(const std::string& file, const string& projectName, bool detach) 24 | { 25 | checkFileExists(file); 26 | auto dir = filesystem::path(file).parent_path(); 27 | auto command 28 | = format("cd \"{}\" && {} -p \"{}\" -f \"{}\" up --build", dir.string(), composeCommand, projectName, file); 29 | if (detach) 30 | command += " -d"; 31 | processExecutor->execOrThrow(command); 32 | } 33 | 34 | void ComposeExecutorImpl::down(const std::string& file, const string& projectName, bool withVolumes) 35 | { 36 | checkFileExists(file); 37 | auto command = format("{} -p \"{}\" -f \"{}\" down", composeCommand, projectName, file); 38 | if (withVolumes) 39 | command += " -v"; 40 | processExecutor->execOrThrow(command); 41 | } 42 | -------------------------------------------------------------------------------- /cli/compose_executor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "process_executor.h" 4 | 5 | class ComposeExecutor { 6 | public: 7 | virtual void up(const std::string& file, const std::string& projectName, bool detach) = 0; 8 | virtual void down(const std::string& file, const std::string& projectName, bool withVolumes) = 0; 9 | }; 10 | 11 | using ComposeExecutorPtr = std::shared_ptr; 12 | 13 | class ComposeExecutorImpl : public ComposeExecutor { 14 | public: 15 | ComposeExecutorImpl(ProcessExecutorPtr processExecutor); 16 | 17 | void up(const std::string& file, const std::string& projectName, bool detach); 18 | void down(const std::string& file, const std::string& projectName, bool withVolumes); 19 | 20 | private: 21 | ProcessExecutorPtr processExecutor; 22 | std::string composeCommand; 23 | }; 24 | -------------------------------------------------------------------------------- /cli/config.h.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define APP_VERSION "@APP_VERSION@" 4 | -------------------------------------------------------------------------------- /cli/dcw_config.cpp: -------------------------------------------------------------------------------- 1 | #include "dcw_config.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace std; 10 | 11 | const std::string DCW_CONFIG_NAME = ".dcw.yml"; 12 | 13 | namespace { 14 | 15 | std::optional tryLoad(const std::string& file) 16 | { 17 | try { 18 | if (!filesystem::exists(file)) 19 | return nullopt; 20 | DcwConfig res; 21 | auto yaml = YAML::LoadFile(file); 22 | res.workspace.name = yaml["name"].as(); 23 | res.workspace.composeFile = yaml["composeFile"].as(); 24 | return res; 25 | } catch (const exception& e) { 26 | throw runtime_error(format("Cannot load config file \"{}\": {}", file, e.what())); 27 | } 28 | } 29 | 30 | } 31 | 32 | void DcwConfig::save(const std::string& file, const DcwConfig& config) 33 | { 34 | try { 35 | auto yaml = YAML::Node(); 36 | yaml["name"] = config.workspace.name; 37 | yaml["composeFile"] = config.workspace.composeFile; 38 | std::ofstream fout(file); 39 | fout.exceptions(ios::badbit | ios::failbit); 40 | fout << "# This is a configuration file of Docker Compose Workspace (dcw) tool" << endl; 41 | fout << "# Read more at https://github.com/navrocky/dcw" << endl; 42 | fout << "" << endl; 43 | fout << yaml; 44 | } catch (const exception& e) { 45 | throw runtime_error(format("Cannot write config file \"{}\": {}", file, e.what())); 46 | } 47 | } 48 | 49 | std::optional DcwConfig::search(const std::string& fileName, const std::string& currentDir) 50 | { 51 | auto dir = filesystem::absolute(currentDir); 52 | while (true) { 53 | auto file = dir / fileName; 54 | if (filesystem::exists(file)) 55 | return file.string(); 56 | if (!dir.has_parent_path()) 57 | return nullopt; 58 | 59 | auto oldDir = dir; 60 | dir = dir.parent_path(); 61 | // stupid std lib realisation, has_parent_path always returns true 62 | if (oldDir == dir) 63 | return nullopt; 64 | } 65 | } 66 | 67 | std::optional DcwConfig::searchAndLoad(const string& fileName, const std::string& currentDir) 68 | { 69 | auto path = search(fileName, currentDir); 70 | return path ? tryLoad(*path) : nullopt; 71 | } 72 | 73 | DcwConfig DcwConfig::load(const std::string& file) 74 | { 75 | auto res = tryLoad(file); 76 | if (!res) 77 | throw runtime_error(format("Config file \"{}\" does not exists", file)); 78 | return *res; 79 | } 80 | -------------------------------------------------------------------------------- /cli/dcw_config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "workspace.h" 7 | 8 | extern const std::string DCW_CONFIG_NAME; 9 | 10 | struct DcwConfig { 11 | 12 | Workspace workspace; 13 | 14 | static void save(const std::string& file, const DcwConfig& config); 15 | static std::optional search(const std::string& fileName, const std::string& currentDir); 16 | static std::optional searchAndLoad(const std::string& fileName, const std::string& currentDir); 17 | static DcwConfig load(const std::string& file); 18 | }; 19 | -------------------------------------------------------------------------------- /cli/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "yaml_workspaces_repository.h" 9 | 10 | #include "commands/add_command.h" 11 | #include "commands/down_command.h" 12 | #include "commands/list_command.h" 13 | #include "commands/remove_command.h" 14 | #include "commands/up_command.h" 15 | #include "config.h" 16 | 17 | using namespace std; 18 | namespace tc = termcolor; 19 | 20 | using Commands = vector; 21 | 22 | int main(int argc, char** argv) 23 | { 24 | try { 25 | auto home = std::getenv("HOME"); 26 | 27 | auto yamlConfig = make_shared(format("{}/.config/dcw/config.yml", home)); 28 | auto yamlState = make_shared(format("{}/.local/share/dcw/state.yml", home)); 29 | 30 | auto workspacesRepo = make_shared(yamlConfig); 31 | auto stateRepo = make_shared(yamlState); 32 | auto processExecutor = make_shared(); 33 | auto composeExecutor = make_shared(processExecutor); 34 | auto workspaceService = make_shared(workspacesRepo, stateRepo, composeExecutor); 35 | 36 | if (argc == 1) { 37 | // if no args provided then print workspaces list 38 | workspaceService->list(false); 39 | return 0; 40 | } 41 | 42 | Commands commands = { 43 | make_shared(workspaceService), 44 | make_shared(workspaceService), 45 | make_shared(workspaceService), 46 | make_shared(workspaceService), 47 | make_shared(workspaceService), 48 | }; 49 | 50 | CLI::App app { format("Docker Compose Workspace manager (v{})", APP_VERSION) }; 51 | for (const auto& cmd : commands) { 52 | cmd->reg(app); 53 | } 54 | 55 | CLI11_PARSE(app, argc, argv); 56 | 57 | return 0; 58 | } catch (const exception& e) { 59 | cerr << tc::red << "❌ Error: " << e.what() << tc::reset << endl; 60 | return 1; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /cli/process_executor.cpp: -------------------------------------------------------------------------------- 1 | #include "process_executor.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | int ProcessExecutor::exec(const std::string& cmdLine) { return std::system(cmdLine.c_str()); } 8 | 9 | void ProcessExecutor::execOrThrow(const std::string& cmdLine) 10 | { 11 | auto res = exec(cmdLine); 12 | if (res != 0) 13 | throw std::runtime_error(std::format("Command exited with status {}: {}", res, cmdLine)); 14 | } 15 | -------------------------------------------------------------------------------- /cli/process_executor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class ProcessExecutor { 7 | public: 8 | int exec(const std::string& cmdLine); 9 | void execOrThrow(const std::string& cmdLine); 10 | }; 11 | 12 | using ProcessExecutorPtr = std::shared_ptr; 13 | -------------------------------------------------------------------------------- /cli/state_repository.cpp: -------------------------------------------------------------------------------- 1 | #include "state_repository.h" 2 | 3 | using namespace std; 4 | 5 | static const char* CURRENT_WORKSPACE_KEY = "current-workspace"; 6 | 7 | YamlStateRepository::YamlStateRepository(const YamlConfigPtr& state) 8 | : state(state) 9 | { 10 | } 11 | 12 | std::optional YamlStateRepository::getCurrentWorkspace() const 13 | { 14 | auto config = state->getConfig(); 15 | auto n = config[CURRENT_WORKSPACE_KEY]; 16 | if (n.IsDefined()) { 17 | return n.as(); 18 | } else { 19 | return std::nullopt; 20 | } 21 | } 22 | 23 | void YamlStateRepository::setCurrentWorkspace(const std::optional& name) const 24 | { 25 | auto config = state->getConfig(); 26 | if (name.has_value()) { 27 | config[CURRENT_WORKSPACE_KEY] = *name; 28 | } else { 29 | config.remove(CURRENT_WORKSPACE_KEY); 30 | } 31 | state->save(); 32 | } 33 | -------------------------------------------------------------------------------- /cli/state_repository.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "yaml_config.h" 7 | 8 | class StateRepository { 9 | public: 10 | ~StateRepository() { } 11 | virtual std::optional getCurrentWorkspace() const = 0; 12 | virtual void setCurrentWorkspace(const std::optional& name) const = 0; 13 | }; 14 | 15 | using StateRepositoryPtr = std::shared_ptr; 16 | 17 | class YamlStateRepository : public StateRepository { 18 | public: 19 | YamlStateRepository(const YamlConfigPtr& state); 20 | 21 | std::optional getCurrentWorkspace() const override; 22 | void setCurrentWorkspace(const std::optional& name) const override; 23 | 24 | private: 25 | YamlConfigPtr state; 26 | }; 27 | -------------------------------------------------------------------------------- /cli/workspace.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct Workspace { 6 | std::string name; 7 | std::string composeFile; 8 | }; 9 | -------------------------------------------------------------------------------- /cli/workspace_service.cpp: -------------------------------------------------------------------------------- 1 | #include "workspace_service.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "dcw_config.h" 9 | 10 | using namespace std; 11 | namespace tc = termcolor; 12 | 13 | WorkspaceService::WorkspaceService( 14 | const WorkspacesRepositoryPtr& repo, const StateRepositoryPtr& stateRepo, const ComposeExecutorPtr& composeExecutor) 15 | : wpRepo(repo) 16 | , stateRepo(stateRepo) 17 | , composeExecutor(composeExecutor) 18 | { 19 | } 20 | 21 | void WorkspaceService::list(bool namesOnly) 22 | { 23 | auto currentWpName = stateRepo->getCurrentWorkspace(); 24 | for (const auto& wp : wpRepo->findAll()) { 25 | if (namesOnly) { 26 | cout << wp.name << endl; 27 | } else { 28 | if (wp.name == currentWpName) 29 | cout << "🟢"; 30 | else 31 | cout << "⚫"; 32 | cout << " " << tc::bold << wp.name << tc::reset << ": " << tc::bright_grey << wp.composeFile << tc::reset 33 | << endl; 34 | } 35 | } 36 | } 37 | 38 | void WorkspaceService::add(const string& name, const string& composeFile, bool createProjectFile) 39 | { 40 | Workspace wp = { .name = name, .composeFile = composeFile }; 41 | wpRepo->add(wp); 42 | cout << "✅ " << "Workspace \"" << tc::bold << name << tc::reset << "\" added" << endl; 43 | if (createProjectFile) { 44 | auto currentDir = filesystem::current_path().string(); 45 | wp.composeFile 46 | = wp.composeFile.substr(currentDir.length() + 1, wp.composeFile.length() - currentDir.length() - 1); 47 | DcwConfig::save(DCW_CONFIG_NAME, { .workspace = wp }); 48 | cout << "✅ " << "Project file \"" << tc::bold << DCW_CONFIG_NAME << tc::reset << "\" created" << endl; 49 | } 50 | } 51 | 52 | void WorkspaceService::remove(const std::string& name) 53 | { 54 | auto currentWpName = stateRepo->getCurrentWorkspace(); 55 | if (currentWpName == name) { 56 | try { 57 | down(true); 58 | } catch (const std::exception& e) { 59 | cerr << "❌ " << "Cannot down current workspace \"" << tc::bold << name << tc::reset 60 | << "\" due to error, skipping. Error is: " << tc::bright_red << e.what() << tc::reset << endl; 61 | } 62 | stateRepo->setCurrentWorkspace(""); 63 | } 64 | wpRepo->remove(name); 65 | cout << "✅ " << "Workspace \"" << tc::bold << name << tc::reset << "\" removed" << endl; 66 | } 67 | 68 | void WorkspaceService::down(bool purge) 69 | { 70 | auto currentWpName = stateRepo->getCurrentWorkspace(); 71 | if (!currentWpName.has_value() || currentWpName->empty()) 72 | return; 73 | auto wp = getWorkspace(currentWpName.value()); 74 | composeExecutor->down(wp.composeFile, wp.name, purge); 75 | stateRepo->setCurrentWorkspace(std::nullopt); 76 | cout << "✅ " << "Workspace \"" << tc::bold << *currentWpName << tc::reset << "\" stopped" << endl; 77 | if (purge) 78 | cout << "✅ " << "Workspace \"" << tc::bold << *currentWpName << tc::reset << "\" data removed" << endl; 79 | } 80 | 81 | void WorkspaceService::up(const std::string& name, bool clean) 82 | { 83 | auto currentWpName = stateRepo->getCurrentWorkspace(); 84 | optional wp; 85 | 86 | if (!name.empty()) { 87 | wp = getWorkspace(name); 88 | } 89 | 90 | if (!wp.has_value()) { 91 | wp = loadWorkspaceFromConfig(); 92 | if (wp) { 93 | auto existingWp = wpRepo->findByName(wp->name); 94 | if (!existingWp) 95 | add(wp->name, filesystem::absolute(wp->composeFile), false); 96 | } else { 97 | auto curPath = filesystem::current_path(); 98 | wp = findWorkspaceByPath(curPath); 99 | } 100 | } 101 | 102 | if (!wp.has_value() && currentWpName.has_value()) { 103 | wp = getWorkspace(*currentWpName); 104 | } 105 | 106 | if (!wp.has_value()) 107 | throw runtime_error("Workspace name required"); 108 | 109 | if (currentWpName.has_value() && *currentWpName != wp->name) 110 | down(false); 111 | if (clean) 112 | composeExecutor->down(wp->composeFile, wp->name, true); 113 | try { 114 | composeExecutor->up(wp->composeFile, wp->name, true); 115 | } catch (...) { 116 | cerr << "❌ " << "Cannot start workspace \"" << tc::bold << wp->name << tc::reset 117 | << "\". Shutting down partially started containers." << endl; 118 | composeExecutor->down(wp->composeFile, wp->name, false); 119 | throw; 120 | } 121 | 122 | stateRepo->setCurrentWorkspace(wp->name); 123 | cout << "✅ " << "Workspace \"" << tc::bold << wp->name << tc::reset << "\" activated" << endl; 124 | } 125 | 126 | Workspace WorkspaceService::getWorkspace(const std::string& name) const 127 | { 128 | auto wp = wpRepo->findByName(name); 129 | if (!wp.has_value()) 130 | throw runtime_error(format("Workspace \"{}\" not found", name)); 131 | return *wp; 132 | } 133 | 134 | std::optional WorkspaceService::findWorkspaceByPath(const std::string& path) const 135 | { 136 | vector workspaces; 137 | for (const auto& wp : wpRepo->findAll()) { 138 | if (wp.composeFile.starts_with(path)) 139 | workspaces.push_back(wp); 140 | } 141 | return workspaces.size() == 1 ? optional(workspaces[0]) : nullopt; 142 | } 143 | 144 | std::optional WorkspaceService::loadWorkspaceFromConfig() 145 | { 146 | auto curPath = filesystem::current_path(); 147 | auto configFile = DcwConfig::search(DCW_CONFIG_NAME, curPath); 148 | if (!configFile) 149 | return nullopt; 150 | 151 | auto config = DcwConfig::load(*configFile); 152 | if (filesystem::path(config.workspace.composeFile).is_relative()) { 153 | auto configFilePath = filesystem::path(*configFile); 154 | auto projectRootPath = configFilePath.has_parent_path() ? configFilePath.parent_path() : "/"; 155 | config.workspace.composeFile = projectRootPath / config.workspace.composeFile; 156 | } 157 | 158 | return config.workspace; 159 | } 160 | -------------------------------------------------------------------------------- /cli/workspace_service.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "compose_executor.h" 6 | #include "state_repository.h" 7 | #include "workspaces_repository.h" 8 | 9 | class WorkspaceService { 10 | public: 11 | WorkspaceService(const WorkspacesRepositoryPtr& repo, const StateRepositoryPtr& stateRepo, 12 | const ComposeExecutorPtr& composeExecutor); 13 | 14 | void list(bool namesOnly); 15 | void add(const std::string& name, const std::string& composeFile, bool createProjectFile); 16 | void remove(const std::string& name); 17 | void stop(); 18 | void down(bool purge); 19 | void up(const std::string& name, bool clean); 20 | 21 | private: 22 | Workspace getWorkspace(const std::string& name) const; 23 | std::optional findWorkspaceByPath(const std::string& path) const; 24 | std::optional loadWorkspaceFromConfig(); 25 | 26 | WorkspacesRepositoryPtr wpRepo; 27 | StateRepositoryPtr stateRepo; 28 | ComposeExecutorPtr composeExecutor; 29 | }; 30 | 31 | using WorkspaceServicePtr = std::shared_ptr; 32 | -------------------------------------------------------------------------------- /cli/workspaces_repository.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "workspace.h" 9 | 10 | class WorkspacesRepository { 11 | public: 12 | virtual ~WorkspacesRepository() { } 13 | virtual void add(const Workspace& w) = 0; 14 | virtual void remove(const std::string& name) = 0; 15 | virtual std::vector findAll() = 0; 16 | virtual std::optional findByName(const std::string& name) = 0; 17 | }; 18 | 19 | using WorkspacesRepositoryPtr = std::shared_ptr; 20 | -------------------------------------------------------------------------------- /cli/yaml_config.cpp: -------------------------------------------------------------------------------- 1 | #include "yaml_config.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | YamlConfig::YamlConfig(const std::string& file) 8 | : initialized(false) 9 | , file(file) 10 | { 11 | } 12 | 13 | void YamlConfig::setFile(const std::string& file) 14 | { 15 | initialized = false; 16 | this->file = file; 17 | } 18 | 19 | YAML::Node& YamlConfig::getConfig() 20 | { 21 | if (!initialized) { 22 | if (std::filesystem::exists(file)) { 23 | config = YAML::LoadFile(file); 24 | } else { 25 | config = YAML::Node(); 26 | } 27 | initialized = true; 28 | } 29 | return config; 30 | } 31 | 32 | void YamlConfig::save() 33 | { 34 | if (!initialized) 35 | return; 36 | std::filesystem::create_directories(std::filesystem::path(file).parent_path()); 37 | std::ofstream fout(file); 38 | fout << config; 39 | } 40 | -------------------------------------------------------------------------------- /cli/yaml_config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | class YamlConfig { 8 | public: 9 | YamlConfig(const std::string& file); 10 | 11 | void setFile(const std::string& file); 12 | 13 | YAML::Node& getConfig(); 14 | 15 | void save(); 16 | 17 | private: 18 | bool initialized; 19 | std::string file; 20 | YAML::Node config; 21 | }; 22 | 23 | using YamlConfigPtr = std::shared_ptr; 24 | -------------------------------------------------------------------------------- /cli/yaml_workspaces_repository.cpp: -------------------------------------------------------------------------------- 1 | #include "yaml_workspaces_repository.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | using namespace std; 8 | 9 | const char* WORKSPACES_KEY = "workspaces"; 10 | const char* COMPOSE_FILE_KEY = "compose-file"; 11 | 12 | YamlWorkspacesRepository::YamlWorkspacesRepository(const YamlConfigPtr& file) 13 | : file(file) 14 | { 15 | } 16 | 17 | void YamlWorkspacesRepository::add(const Workspace& w) 18 | { 19 | auto existing = findByName(w.name); 20 | if (existing) 21 | throw runtime_error(format("Workspace with name '{}' already exists", w.name)); 22 | 23 | auto& config = file->getConfig(); 24 | YAML::Node wp; 25 | wp[COMPOSE_FILE_KEY] = w.composeFile; 26 | config[WORKSPACES_KEY][w.name] = wp; 27 | file->save(); 28 | } 29 | 30 | void YamlWorkspacesRepository::remove(const std::string& name) 31 | { 32 | auto wp = findByName(name); 33 | if (!wp.has_value()) 34 | throw runtime_error(format("Workspace \"{}\" not found", name)); 35 | 36 | auto& config = file->getConfig(); 37 | auto workspaces = config[WORKSPACES_KEY]; 38 | workspaces.remove(name); 39 | file->save(); 40 | } 41 | 42 | std::vector YamlWorkspacesRepository::findAll() 43 | { 44 | auto& config = file->getConfig(); 45 | auto wp = config[WORKSPACES_KEY]; 46 | 47 | if (!wp.IsDefined()) 48 | return std::vector(); 49 | 50 | if (!wp.IsMap()) 51 | throw runtime_error("Workspaces is not a map"); 52 | 53 | std::vector result; 54 | for (const auto& pair : wp) { 55 | result.push_back( 56 | { .name = pair.first.as(), .composeFile = pair.second[COMPOSE_FILE_KEY].as() }); 57 | } 58 | return result; 59 | } 60 | 61 | std::optional YamlWorkspacesRepository::findByName(const std::string& name) 62 | { 63 | auto& config = file->getConfig(); 64 | auto wp = config[WORKSPACES_KEY][name]; 65 | if (!wp.IsDefined()) 66 | return std::nullopt; 67 | return Workspace { .name = name, .composeFile = wp[COMPOSE_FILE_KEY].as() }; 68 | } 69 | -------------------------------------------------------------------------------- /cli/yaml_workspaces_repository.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "workspaces_repository.h" 4 | #include "yaml_config.h" 5 | 6 | class YamlWorkspacesRepository : public WorkspacesRepository { 7 | public: 8 | YamlWorkspacesRepository(const YamlConfigPtr& file); 9 | void add(const Workspace& w) override; 10 | void remove(const std::string& name) override; 11 | std::vector findAll() override; 12 | std::optional findByName(const std::string& name) override; 13 | 14 | private: 15 | YamlConfigPtr file; 16 | }; 17 | -------------------------------------------------------------------------------- /completion.bash: -------------------------------------------------------------------------------- 1 | function _complete() 2 | { 3 | LATEST="${COMP_WORDS[$COMP_CWORD]}" 4 | 5 | STATE="init" 6 | for ((i=0; i< $COMP_CWORD; i++)) do 7 | WORD=${COMP_WORDS[$i]} 8 | WORDS="" 9 | case "${STATE}" in 10 | init) 11 | case "${WORD}" in 12 | dcw) 13 | STATE="dcw" 14 | WORDS="add down list rm up --help" 15 | ;; 16 | *) 17 | STATE="fail" 18 | ;; 19 | esac 20 | ;; 21 | dcw) 22 | case "${WORD}" in 23 | add) 24 | STATE="add_name" 25 | WORDS="--create-project" 26 | ;; 27 | up) 28 | STATE="up" 29 | WORDS="$(dcw list -n)" 30 | ;; 31 | rm) 32 | STATE="rm" 33 | WORDS=`dcw list -n` 34 | ;; 35 | list) 36 | STATE="list" 37 | WORDS="--names" 38 | ;; 39 | down) 40 | STATE="down" 41 | WORDS="--purge" 42 | ;; 43 | --help) 44 | STATE="help" 45 | WORDS="add down list rm up" 46 | ;; 47 | *) 48 | STATE="fail" 49 | ;; 50 | esac 51 | ;; 52 | add_name) 53 | STATE="add_file" 54 | WORDS=`ls` 55 | ;; 56 | up) 57 | WORDS="--clean" 58 | STATE="up_name" 59 | ;; 60 | *) 61 | ;; 62 | esac 63 | done 64 | 65 | COMPREPLY=($(compgen -W "$WORDS" -- $LATEST)) 66 | return 0 67 | } 68 | 69 | complete -F _complete dcw 70 | -------------------------------------------------------------------------------- /conanfile.txt: -------------------------------------------------------------------------------- 1 | [requires] 2 | termcolor/2.1.0 3 | #boost/1.84.0 4 | yaml-cpp/0.8.0 5 | cli11/2.4.2 6 | 7 | [generators] 8 | CMakeDeps 9 | CMakeToolchain 10 | 11 | [options] 12 | *:shared=False 13 | boost*:numa=False 14 | boost*:zlib=False 15 | boost*:bzip2=False 16 | boost*:without_*=True 17 | boost*:without_program_options=False 18 | boost*:without_log=True 19 | boost*:without_math=True 20 | boost*:without_test=True 21 | boost*:without_wave=True 22 | boost*:without_fiber=True 23 | boost*:without_graph=True 24 | boost*:without_random=True 25 | boost*:without_context=True 26 | boost*:without_coroutine=True 27 | boost*:without_stacktrace=True 28 | boost*:without_iostreams=True 29 | boost*:without_atomic=True 30 | boost*:without_container=True 31 | boost*:without_contract=True 32 | boost*:without_thread=True 33 | boost*:without_chrono=True 34 | boost*:without_filesystem=True 35 | boost*:without_json=True 36 | boost*:without_nowide=True 37 | boost*:without_type_erasure=True 38 | boost*:without_serialization=True 39 | boost*:without_regex=True 40 | boost*:without_timer=True 41 | boost*:without_url=True 42 | boost*:without_locale=True 43 | boost*:i18n_backend_iconv=libiconv 44 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | curl -fL https://github.com/navrocky/dcw/releases/download/1.3.1/dcw -o /usr/local/bin/dcw 6 | chmod +x /usr/local/bin/dcw 7 | curl -fL https://github.com/navrocky/dcw/raw/master/completion.bash -o /etc/bash_completion.d/dcw_completion.bash 8 | 9 | echo "== Installation completed successfully ==" 10 | -------------------------------------------------------------------------------- /reformat_sources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | find . \( -iname *.h -o -iname *.cpp \) -a -not -path "./lib/3rdparty/*" | xargs clang-format -i 4 | -------------------------------------------------------------------------------- /update_changelog_from_git.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | CHANGELOG="CHANGELOG.md" 6 | 7 | getLastTagFromChangeLog() { 8 | grep -oE "([0-9X]+\.){2}[0-9X]" <"$1" | head -n1 9 | } 10 | 11 | printNewRelease() { 12 | LAST_VERSION="$1" 13 | 14 | echo "# X.X.X ($(date +%Y-%m-%d))" 15 | echo 16 | 17 | if [ -z "${LAST_VERSION}" ]; then 18 | COMMITS="HEAD" 19 | else 20 | COMMITS="${LAST_VERSION}..HEAD" 21 | fi 22 | GIT_LOG_COMMAND="git log --format=%s ${COMMITS}" 23 | $GIT_LOG_COMMAND | while IFS=$'\n' read -r LINE; do 24 | echo "- $LINE" 25 | done 26 | 27 | echo 28 | } 29 | 30 | generateChangeLog() { 31 | printNewRelease >>${CHANGELOG} 32 | 33 | echo "The initial ${CHANGELOG} generated" 34 | } 35 | 36 | updateChangeLog() { 37 | LAST_VERSION="$1" 38 | 39 | if [ "${LAST_VERSION}" == "X.X.X" ]; then 40 | echo "The ${CHANGELOG} already contains raw git log" 41 | exit 1 42 | fi 43 | 44 | TMP_CHANGELOG="${CHANGELOG}.tmp" 45 | [ -f "${TMP_CHANGELOG}" ] && rm ${TMP_CHANGELOG} 46 | 47 | # copy original change log and insert new release 48 | cat ${CHANGELOG} | while IFS=$'\n' read -r LINE; do 49 | if [[ "${LINE}" == *"${LAST_VERSION}"* ]]; then 50 | printNewRelease "${LAST_VERSION}" >>${TMP_CHANGELOG} 51 | fi 52 | echo "${LINE}" >>${TMP_CHANGELOG} 53 | done 54 | 55 | mv ${TMP_CHANGELOG} ${CHANGELOG} 56 | 57 | echo "The ${CHANGELOG} updated with a recent commits after the tag ${LAST_VERSION}" 58 | } 59 | 60 | if [ -f "${CHANGELOG}" ]; then 61 | LAST_VERSION=$(getLastTagFromChangeLog ${CHANGELOG}) 62 | fi 63 | 64 | if [ -z "${LAST_VERSION}" ]; then 65 | generateChangeLog 66 | else 67 | updateChangeLog "${LAST_VERSION}" 68 | fi 69 | --------------------------------------------------------------------------------