├── .flake8 ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── FEATURE_REQUEST.md │ ├── OTHER.md │ └── SUPPORT.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── schedule.yml │ ├── stale.yml │ └── test.yml ├── .gitignore ├── CHANGES ├── CONTRIBUTORS ├── LICENSE ├── Makefile ├── README.md ├── bootstrap ├── completion ├── README.md ├── bash │ └── yadm ├── fish │ └── yadm.fish └── zsh │ └── _yadm ├── contrib ├── bootstrap │ └── bootstrap-in-dir ├── commands │ ├── README.md │ └── yadm-untracked └── hooks │ ├── README.md │ ├── encrypt_with_checksums │ ├── README.md │ ├── post_encrypt │ ├── post_list │ └── post_status │ └── parsing_full_command_example │ ├── README.md │ └── pre_log ├── pyproject.toml ├── test ├── Dockerfile ├── conftest.py ├── ownertrust.txt ├── pinentry-mock ├── requirements.txt ├── test_alt.py ├── test_alt_copy.py ├── test_assert_private_dirs.py ├── test_bootstrap.py ├── test_clean.py ├── test_clone.py ├── test_config.py ├── test_encryption.py ├── test_enter.py ├── test_ext_crypt.py ├── test_git.py ├── test_help.py ├── test_hooks.py ├── test_init.py ├── test_introspect.py ├── test_key ├── test_list.py ├── test_perms.py ├── test_syntax.py ├── test_unit_bootstrap_available.py ├── test_unit_choose_template_processor.py ├── test_unit_configure_paths.py ├── test_unit_copy_perms.py ├── test_unit_encryption.py ├── test_unit_exclude_encrypted.py ├── test_unit_issue_legacy_path_warning.py ├── test_unit_parse_encrypt.py ├── test_unit_private_dirs.py ├── test_unit_query_distro.py ├── test_unit_query_distro_family.py ├── test_unit_record_score.py ├── test_unit_relative_path.py ├── test_unit_report_invalid_alts.py ├── test_unit_score_file.py ├── test_unit_set_local_alt_values.py ├── test_unit_set_os.py ├── test_unit_set_yadm_dir.py ├── test_unit_template_default.py ├── test_unit_template_esh.py ├── test_unit_template_j2.py ├── test_unit_upgrade.py ├── test_unit_x_program.py ├── test_upgrade.py ├── test_version.py └── utils.py ├── yadm ├── yadm.1 ├── yadm.md └── yadm.spec /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | yadm text eol=lf 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help improve yadm 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 16 | 17 | ### Describe the bug 18 | 19 | [A clear and concise description of what the bug is.] 20 | 21 | ### To reproduce 22 | 23 | Can this be reproduced with the yadm/testbed docker image: [Yes/No] 24 | 48 | 49 | Steps to reproduce the behavior: 50 | 51 | 1. Run command '....' 52 | 2. Run command '....' 53 | 3. Run command '....' 54 | 4. See error 55 | 56 | ### Expected behavior 57 | 58 | [A clear and concise description of what you expected to happen.] 59 | 60 | ### Environment 61 | 62 | - Operating system: [Ubuntu 18.04, yadm/testbed, etc.] 63 | - Version yadm: [found via `yadm version`] 64 | - Version Git: [found via `git --version`] 65 | 66 | ### Additional context 67 | 68 | [Add any other context about the problem here.] 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for yadm 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 13 | 14 | ### Is your feature request related to a problem? Please describe. 15 | 16 | [A clear and concise description of what the problem is. Ex. I'm always frustrated when ...] 17 | 18 | ### Describe the solution you'd like 19 | 20 | [A clear and concise description of what you want to happen.] 21 | 22 | ### Describe alternatives you've considered 23 | 24 | [A clear and concise description of any alternative solutions or features you've 25 | considered. For example, have you considered using yadm "hooks" as a solution?] 26 | 27 | ### Additional context 28 | 29 | [Add any other context or screenshots about the feature request here.] 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/OTHER.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other issue 3 | about: Report issues with documentation, packaging, or something else 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 13 | 14 | ### This issue is about 15 | 16 | * [ ] Man pages or command-line usage 17 | * [ ] Website documentation 18 | * [ ] Packaging 19 | * [ ] Other 20 | 21 | ### Describe the issue 22 | 23 | [A clear and concise description of the issue.] 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/SUPPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support 3 | about: Get help using yadm 4 | title: '' 5 | labels: 'question' 6 | assignees: '' 7 | 8 | --- 9 | 17 | 18 | ### This question is about 19 | 20 | * [ ] Installation 21 | * [ ] Initializing / Cloning 22 | * [ ] Alternate files 23 | * [ ] Jinja templates 24 | * [ ] Encryption 25 | * [ ] Bootstrap 26 | * [ ] Hooks 27 | * [ ] Other 28 | 29 | ### Describe your question 30 | 31 | 36 | [A clear and concise description of the question.] 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What does this PR do? 2 | 3 | [A clear and concise description of what this pull request accomplishes.] 4 | 5 | ### What issues does this PR fix or reference? 6 | 7 | 10 | [A list of related issues / pull requests.] 11 | 12 | ### Previous Behavior 13 | 14 | [Describe the existing behavior.] 15 | 16 | ### New Behavior 17 | 18 | [Describe the behavior, after this PR is applied.] 19 | 20 | ### Have [tests][1] been written for this change? 21 | 22 | [Yes / No] 23 | 24 | ### Have these commits been [signed with GnuPG][2]? 25 | 26 | [Yes / No] 27 | 28 | --- 29 | 30 | Please review [yadm's Contributing Guide][3] for best practices. 31 | 32 | [1]: https://github.com/yadm-dev/yadm/blob/master/.github/CONTRIBUTING.md#test-conventions 33 | [2]: https://help.github.com/en/articles/signing-commits 34 | [3]: https://github.com/yadm-dev/yadm/blob/master/.github/CONTRIBUTING.md 35 | -------------------------------------------------------------------------------- /.github/workflows/schedule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Scheduled Site Tests 3 | on: # yamllint disable-line rule:truthy 4 | schedule: 5 | - cron: "0 0 1 * *" # Monthly 6 | jobs: 7 | Tests: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | ref: gh-pages 13 | - run: >- 14 | docker create -t 15 | --name yadm-website 16 | --entrypoint test/validate 17 | yadm/jekyll:2024-10-31; 18 | docker cp ./ yadm-website:/srv/jekyll 19 | - name: Test Site 20 | run: docker start yadm-website -a 21 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Close Stale Issues 3 | on: # yamllint disable-line rule:truthy 4 | schedule: 5 | - cron: "30 1 * * *" # Daily 6 | jobs: 7 | Stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v4 11 | with: 12 | close-issue-message: >- 13 | This issue was closed because it has been labeled as stale for 7 14 | days with no activity. 15 | days-before-close: 7 16 | days-before-stale: 60 17 | exempt-all-assignees: true 18 | exempt-issue-labels: in develop, 1, 2, 3 19 | exempt-pr-labels: in develop, 1, 2, 3 20 | stale-issue-label: stale 21 | stale-issue-message: >- 22 | This issue has been labeled as stale because it has been open 60 23 | days with no activity. Remove stale label or comment or this will 24 | be closed in 7 days. 25 | stale-pr-label: stale 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Tests 3 | 4 | on: # yamllint disable-line rule:truthy 5 | - push 6 | - pull_request 7 | - workflow_dispatch 8 | 9 | env: 10 | SC_VER: "0.10.0" 11 | ESH_VER: "0.3.2" 12 | 13 | jobs: 14 | Tests: 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: 20 | - ubuntu-22.04 21 | - ubuntu-24.04 22 | - macos-13 23 | - macos-15 24 | - windows-2022 25 | 26 | defaults: 27 | run: 28 | shell: bash 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - uses: Vampire/setup-wsl@v4 34 | if: ${{ runner.os == 'Windows' }} 35 | 36 | - name: Install dependencies on Linux 37 | if: ${{ runner.os == 'Linux' }} 38 | run: | 39 | sudo apt-get update 40 | sudo apt-get install -y expect j2cli 41 | 42 | - name: Install dependencies on macOS 43 | if: ${{ runner.os == 'macOS' }} 44 | run: | 45 | command -v expect || brew install expect 46 | 47 | - name: Install dependencies on Windows (WSL) 48 | if: ${{ runner.os == 'Windows' }} 49 | shell: wsl-bash {0} 50 | run: | 51 | apt-get update 52 | apt-get install -y --no-install-recommends \ 53 | dos2unix \ 54 | expect \ 55 | gettext-base \ 56 | git \ 57 | gnupg \ 58 | j2cli \ 59 | lsb-release \ 60 | man \ 61 | python3-pip 62 | 63 | - name: Prepare tools directory 64 | run: | 65 | mkdir "${{ runner.temp }}/tools" 66 | echo "${{ runner.temp }}/tools" >> "${{ github.path }}" 67 | 68 | - name: Install shellcheck 69 | run: | 70 | cd "${{ runner.temp }}" 71 | 72 | OS=${{ runner.os == 'macOS' && 'darwin' || 'linux' }} 73 | ARCH=${{ runner.arch == 'ARM64' && 'aarch64' || 'x86_64' }} 74 | 75 | BASE_URL="https://github.com/koalaman/shellcheck/releases/download" 76 | SC="v$SC_VER/shellcheck-v$SC_VER.$OS.$ARCH.tar.xz" 77 | 78 | curl -L "$BASE_URL/$SC" | tar Jx shellcheck-v$SC_VER/shellcheck 79 | mv shellcheck-v$SC_VER/shellcheck tools 80 | 81 | - name: Install esh 82 | run: | 83 | cd "${{ runner.temp }}/tools" 84 | 85 | BASE_URL="https://github.com/jirutka/esh/raw/refs/tags" 86 | curl -L -o esh "$BASE_URL/v$ESH_VER/esh" 87 | chmod +x esh 88 | 89 | - name: Add old yadm versions # to test upgrades 90 | run: | 91 | for version in 1.12.0 2.5.0; do 92 | git fetch origin $version:refs/tags/$version 93 | git cat-file blob $version:yadm \ 94 | > "${{ runner.temp }}/tools/yadm-$version" 95 | chmod +x "${{ runner.temp }}/tools/yadm-$version" 96 | done 97 | 98 | - name: Set up Python 3.11 99 | if: ${{ runner.os == 'macOS' || matrix.os == 'ubuntu-22.04' }} 100 | uses: actions/setup-python@v5 101 | with: 102 | python-version: 3.11 103 | 104 | - name: Install dependencies and run tests (Linux/macOS) 105 | if: ${{ runner.os != 'Windows' }} 106 | run: | 107 | git config --global user.email test@yadm.io 108 | git config --global user.name "Yadm Test" 109 | 110 | python3 -m pip install --upgrade pip 111 | python3 -m pip install -r test/requirements.txt 112 | pytest -v --color=yes --basetemp="${{ runner.temp }}/pytest" 113 | 114 | - name: Install dependencies and run tests (WSL) 115 | if: ${{ runner.os == 'Windows' }} 116 | shell: wsl-bash {0} 117 | run: | 118 | git config --global user.email test@yadm.io 119 | git config --global user.name "Yadm Test" 120 | git config --global protocol.file.allow always 121 | 122 | dos2unix yadm.1 .github/workflows/*.yml test/pinentry-mock 123 | chmod +x test/pinentry-mock 124 | 125 | python3 -m pip install --upgrade pip 126 | python3 -m pip install -r test/requirements.txt 127 | pytest -v --color=yes 128 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .jekyll-metadata 3 | .pytest_cache 4 | .sass-cache 5 | .testyadm 6 | _site 7 | testenv 8 | __pycache__/ 9 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 3.5.0 2 | * Silence warnings when collecting alt files (#521) 3 | * Adjust handling of encrypt patterns to match 3.3.0 and older 4 | * Make encrypt exclude patterns only match encrypted files 5 | * Automatically exclude alt and template files (#234) 6 | * Support negative alt conditions (#365) 7 | * Handle filenames with space in bash completion (#341) 8 | * Add new yadm.filename template variable (#520) 9 | 10 | 3.4.0 11 | * Improve and harden alt file regeneration (#466) 12 | * Fix "yadm config" in fish completion (#491) 13 | * Fix "yadm clone" when not run in "$YADM_WORK" (#513) 14 | * Output the actual paths in help message (#376) 15 | * Verify all alt conditions for templates (#478) 16 | * Ignore case in alt and default template conditions (#455, #456) 17 | * Fall back to ID for distro family if ID_LIKE is not available (#494) 18 | * Support overriding distro and distro family (#430) 19 | * Improve support for Bash 3 (the default version on macOS) 20 | * Make "yadm clone --recursive" work as expected (#517) 21 | * Don't include files multiple times in archive (#125) 22 | * Document YADM_HOOK_DATA and YADM_HOOK_DIR env variables (#343) 23 | * Support alt dirs with deeply nested tracked files (#495) 24 | 25 | 3.3.0 26 | * Support nested ifs in default template (#436) 27 | * Support include and ifs in default template includes (#406) 28 | * Support environment variables in ifs in default template (#488) 29 | * Support != in default template (#358, #477) 30 | * Fix multiple classes in default template on macOS (#437) 31 | 32 | 3.2.2 33 | * Support spaces in distro/distro-family (#432) 34 | * Fix zsh hanging when tab completing add/checkout (#417) 35 | * Add yadm-untracked script to contributed files (#418) 36 | * Fix documentation typos (#425) 37 | * Support docker-like OCI engines for dev testing (#431) 38 | 39 | 3.2.1 40 | * Fix Bash 3 bad array subscript bug (#411) 41 | 42 | 3.2.0 43 | * Support architecture for alternates/templates (#202, #203, #393) 44 | * Support distro_family for alternates/templates (#213) 45 | * Support setting multiple classes (#185, #304) 46 | * Support environment variables in default template processor (#347) 47 | * Update version command to include Bash & Git versions (#377) 48 | 49 | 3.1.1 50 | * Fix clone support for older versions of Git (#348) 51 | * Fix support for multiple GPG recipients (#342) 52 | * Find symlinks in bootstrap-in-dir (#340) 53 | 54 | 3.1.0 55 | * Use `git clone` directly during clone (#289, #323) 56 | * Fix compatibility bug with Git completions (#318, #321) 57 | * Support relative paths for --yadm-* and -w (#301) 58 | * Improve parsing of if-statement in default template (#303) 59 | * Read files without running cat in subshells (#317) 60 | * Improve portability of updating read-only files (#320) 61 | * Various code improvements (#306, #307, #311) 62 | 63 | 3.0.2 64 | * Fix parsing by sh (#299) 65 | 66 | 3.0.1 67 | * Improve handling of submodules at upgrade (#284, #285, #293) 68 | * Improve Zsh completions (#292, #298) 69 | * Use stderr for error messages (#297) 70 | 71 | 3.0.0 72 | * Support encryption with OpenSSL (#138) 73 | * Support "include" directive in built-in template processor (#255) 74 | * Support extensions for alternate files and templates (#257) 75 | * Improve support for default branches (#231, #232) 76 | * Add --version and --help as yadm internal commands (#267) 77 | * Improve support for XDG base directory specification 78 | * Use XDG_DATA_HOME used for encrypted data and repository (#208) 79 | * Default repo is now ~/.local/share/yadm/repo.git 80 | * Default encrypted archive is now ~/.local/share/yadm/archive 81 | * Improve shell completions (#238, #274, #275) 82 | * Remove support for YADM_COMPATIBILITY=1 (#242) 83 | * Remove deprecated option cygwin-copy 84 | * Fix template mode inheritance on FreeBSD (#243, #246) 85 | * Fix hook execution under MinGW (#150) 86 | * Improve compatibility with Oil shell (#210) 87 | 88 | 2.5.0 89 | * Support for transcrypt (#197) 90 | * Support ESH templates (#220) 91 | * Preserve file mode of template (#193) 92 | * Fish shell completions (#224) 93 | * Fix alt processing when worktree is `/` (#198) 94 | * Assert config directory if missing (#226, #227) 95 | * Documentation improvements (#229) 96 | 97 | 2.4.0 98 | * Support multiple keys in `yadm.gpg-recipient` (#139) 99 | * Ensure all templates are written atomically (#142) 100 | * Add encrypt_with_checksums to the hooks collection (#188) 101 | * Escape white space in YADM_HOOK_FULL_COMMAND (#187) 102 | * Improve parsing of os-release (#194) 103 | * Improve identification of WSL (#196) 104 | * Fix troff warnings emitted by man page (#195) 105 | * Write encrypt-based exclusions during decrypt 106 | 107 | 2.3.0 108 | * Support git-crypt (#168) 109 | * Support specifying a command after `yadm enter` 110 | * Expose GIT_WORK_TREE during `yadm enter` (#160) 111 | * Support GNUPGHOME environment variable (#134) 112 | * Assert private dirs, only when worktree = $HOME (#171) 113 | 114 | 2.2.0 115 | * Resolve hostname using `uname -n` (#182) 116 | * Use /etc/os-release if lsb_release is missing (#175) 117 | * Issue warning for any invalid alternates found (#183) 118 | * Add support for gawk (#180) 119 | 120 | 2.1.0 121 | * Use relative symlinks for alternates (#100, #177) 122 | * Support double-star globs in .config/yadm/encrypt (#109) 123 | * Improve bash completion (#136) 124 | * Update docs about using magit (#123) 125 | * Note exception for WSL (#113) 126 | 127 | 2.0.1 128 | * Fix bug with worktree permissions (#174) 129 | 130 | 2.0.0 131 | * Support XDG base directory specification 132 | * Redesign alternate processing 133 | * Add built-in default template processor 134 | * Allow storing alternates in yadm dir (#90) 135 | * Add support for j2cli template processor 136 | * Ignore encrypted files (#69) 137 | * Support DISTRO in alternates (#72) 138 | * Support `source` in templates (#163) 139 | * Change yadm.cygwin-copy to yadm.alt-copy 140 | * Support `-b ` when cloning (#133) 141 | * Support includes for j2-based templates (#114) 142 | * Remove stale/invalid linked alternates (#65) 143 | * Add support for Mingw/Msys (#102) 144 | * Allow `-l` to pass thru to the `yadm config` command 145 | * Improve processing of `yadm/encrypt` 146 | * Fix bugs in legacy alternate processing 147 | * Fix bug with hidden private files 148 | * Improve support for older versions of Git 149 | * Add upgrade command 150 | 151 | 1.12.0 152 | * Add basic Zsh completion (#71, #79) 153 | * Support directories in `.yadm/encrypt` (#81, #82) 154 | * Support exclusions in `.yadm/encrypt` (#86) 155 | * Improve portability with printf (#87) 156 | * Eliminate usage of `eval` and `ls` 157 | 158 | 1.11.1 159 | * Create private dirs prior to merge (#74) 160 | 161 | 1.11.0 162 | * Option for Cygwin to copy files instead of symlink (#62) 163 | * Support `YADM_DISTRO` in Jinja templates (#68) 164 | * Support pre/post hooks for every command (#70) 165 | 166 | 1.10.0 167 | * Fix `COMP_WORDS bad array subscript` bug (#64) 168 | * Transition to semantic versioning 169 | 170 | 1.09 171 | * Add Bash completion script (#60) 172 | * Support WSL detection (#61) 173 | * Add introspect command (used by completion) 174 | 175 | 1.08 176 | * Fix bug alternates based on `CLASS` (#51) 177 | * Support globs and paths with space in .yadm/encrypt (#53, #54) 178 | * Add support for alternate files using Jinja templates (#56, #58) 179 | * Add `enter` command, for creating a sub-shell (#57) 180 | * Support local.hostname properly (#59) 181 | 182 | 1.07 183 | * Add `CLASS` to supported alt-link patterns (#21) 184 | * Add bootstrap command (#42) 185 | * Support wildcards for alt-links (#43) 186 | * Stash conflicting data during clone (#44) 187 | * Offer bootstrap after successful clone (#45) 188 | * Display supported configs for `yadm config` (#46) 189 | * Add "curl-pipe" program to clone without installation (#48) 190 | * Fix bug in alt-link regular expressions (#49) 191 | 192 | 1.06 193 | * Improve portability of `hostname` (#23) 194 | * Fix incompatibilities between Cygwin and Git for Windows (#26) 195 | * Allow Git program to be configured via yadm.git-program (#30) 196 | * Support alt-links for encrypted files (#34) 197 | * Exit with the same return value as Git (#35) 198 | * Support spaces in alt-link paths (#36) 199 | * Ignore empty lines in .yadm/encrypt (#40) 200 | * Fix typos (#41) 201 | 202 | 1.05 203 | * Improve portability of shebang line (#14) 204 | * Support for symlinked directories (#17) 205 | * Improve portability of tar parameters (#18) 206 | * Support alternate gpg program (#19) 207 | * Fallback to using `ls` if `/bin/ls` does not exist (#22) 208 | 209 | 1.04 210 | * Support alternate paths for yadm data (#4, #5) 211 | * Support asymmetric encryption (#7, #8) 212 | * Prevent the mixing of output and gpg prompts 213 | 214 | 1.03 215 | * Add username matching for alternate files (#1) 216 | 217 | 1.02 218 | * Handle permissions for `~/.gnupg/*gpg` 219 | 220 | 1.01 221 | * Set `status.showUntrackedFiles` to "no" 222 | 223 | 1.00 224 | * Initial public release 225 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | CONTRIBUTORS 2 | 3 | Tim Byrne 4 | Erik Flodin 5 | Martin Zuther 6 | Ross Smith II 7 | Jan Schulz 8 | Jonathan Daigle 9 | Luis López 10 | Tin Lai 11 | Espen Henriksen 12 | AaronYoung5 13 | Cameron Eagans 14 | Klas Mellbourn 15 | James Clark 16 | Glenn Waters 17 | Nicolas signed-log FORMICHELLA 18 | Tomas Cernaj 19 | AVM.Martin 20 | Joshua Cold 21 | jonasc 22 | Nicolas stig124 FORMICHELLA 23 | Chad Wade Day, Jr 24 | Sébastien Gross 25 | Christof Warlich 26 | David Mandelberg 27 | Paulo Köch 28 | Oren Zipori 29 | Daniel Gray 30 | Paraplegic Racehorse 31 | Siôn Le Roux 32 | Mateusz Piotrowski 33 | japm48 34 | Uroš Golja 35 | Satoshi Ohki 36 | Jonas 37 | Franciszek Madej 38 | Daniel Wagenknecht 39 | Stig Palmquist 40 | Patrick Hof 41 | Samisafool 42 | LFdev 43 | con-f-use 44 | Bram Ceulemans 45 | Travis A. Everett 46 | Sheng Yang 47 | Jared Smartt 48 | Adam Jimerson 49 | Tim Condit 50 | Thomas Luzat 51 | Russ Allbery 52 | Patrick Roddy 53 | heddxh 54 | dessert1 55 | Brayden Banks 56 | Alexandre GV 57 | addshore 58 | Felipe S. S. Schneider 59 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTESTS = $(wildcard test/test_*.py) 2 | IMAGE = docker.io/yadm/testbed:2024-11-11 3 | OCI = docker 4 | 5 | .PHONY: all 6 | all: 7 | @$(MAKE) usage | less 8 | 9 | # Display usage for all make targets 10 | .PHONY: usage 11 | usage: 12 | @echo 13 | @echo 'make TARGET [option=value, ...]' 14 | @echo 15 | @echo 'TESTING' 16 | @echo 17 | @echo ' make test [testargs=ARGS]' 18 | @echo ' - Run all tests. "testargs" can specify a single string of arguments' 19 | @echo ' for py.test.' 20 | @echo 21 | @echo ' make .py [testargs=ARGS]' 22 | @echo ' - Run tests from a specific test file. "testargs" can specify a' 23 | @echo ' single string of arguments for py.test.' 24 | @echo 25 | @echo ' make testhost [version=VERSION]' 26 | @echo ' - Create an ephemeral container for doing adhoc yadm testing. The' 27 | @echo ' working copy version of yadm will be used unless "version" is' 28 | @echo ' specified. "version" can be set to any commit, branch, tag, etc.' 29 | @echo ' The targeted "version" will be retrieved from the repo, and' 30 | @echo ' linked into the container as a local volume.' 31 | @echo 32 | @echo ' make scripthost [version=VERSION]' 33 | @echo ' - Create an ephemeral container for demonstrating a bug. After' 34 | @echo ' exiting the shell, a log of the commands used to illustrate the' 35 | @echo ' problem will be written to the file "script.txt". This file can' 36 | @echo ' be useful to developers to make a repeatable test for the' 37 | @echo ' problem. The version parameter works as for "testhost" above.' 38 | @echo 39 | @echo 'LINTING' 40 | @echo 41 | @echo ' make testenv' 42 | @echo ' - Create a python virtual environment with the same dependencies' 43 | @echo " used by yadm's testbed environment. Creating and activating" 44 | @echo ' this environment might be useful if your editor does real time' 45 | @echo ' linting of python files. After creating the virtual environment,' 46 | @echo ' you can activate it by typing:' 47 | @echo 48 | @echo ' source testenv/bin/activate' 49 | @echo 50 | @echo 'MANPAGES' 51 | @echo 52 | @echo ' make man' 53 | @echo ' - View yadm.1 as a standard man page.' 54 | @echo 55 | @echo ' make man-wide' 56 | @echo ' - View yadm.1 as a man page, using all columns of your display.' 57 | @echo 58 | @echo ' make man-ps' 59 | @echo ' - Create a postscript version of the man page.' 60 | @echo 61 | @echo 'FILE GENERATION' 62 | @echo 63 | @echo ' make yadm.md' 64 | @echo ' - Generate the markdown version of the man page (for viewing on' 65 | @echo ' the web).' 66 | @echo 67 | @echo ' make contrib' 68 | @echo ' - Generate the CONTRIBUTORS file, from the repo history.' 69 | @echo 70 | @echo 'INSTALLATION' 71 | @echo 72 | @echo ' make install PREFIX=' 73 | @echo ' - Install yadm, manpage, etc. to ' 74 | @echo 75 | @echo 'UTILITIES' 76 | @echo 77 | @echo ' make sync-clock' 78 | @echo ' - Reset the hardware clock for the docker hypervisor host. This' 79 | @echo ' can be useful for docker engine hosts which are not' 80 | @echo ' Linux-based.' 81 | @echo 82 | 83 | # Make it possible to run make specifying a py.test test file 84 | .PHONY: $(PYTESTS) 85 | $(PYTESTS): 86 | @$(MAKE) test testargs="$@ $(testargs)" 87 | %.py: 88 | @$(MAKE) test testargs="-k $@ $(testargs)" 89 | 90 | # Run all tests with additional testargs 91 | .PHONY: test 92 | test: 93 | @if [ -f /.yadmtestbed ]; then \ 94 | cd /yadm && \ 95 | py.test -v $(testargs); \ 96 | else \ 97 | $(MAKE) -s require-docker && \ 98 | $(OCI) run \ 99 | --rm -t$(shell test -t 0 && echo i) \ 100 | -v "$(CURDIR):/yadm:ro" \ 101 | $(IMAGE) \ 102 | make test testargs="$(testargs)"; \ 103 | fi 104 | 105 | .PHONY: .testyadm 106 | .testyadm: version ?= local 107 | .testyadm: 108 | @rm -f $@ 109 | @if [ "$(version)" = "local" ]; then \ 110 | ln -sf yadm $@; \ 111 | echo "Using local yadm ($$(git describe --tags --dirty))"; \ 112 | else \ 113 | git show $(version):yadm > $@; \ 114 | echo "Using yadm version $$(git describe --tags $(version))"; \ 115 | fi 116 | @chmod a+x $@ 117 | 118 | .PHONY: testhost 119 | testhost: require-docker .testyadm 120 | @echo "Starting testhost" 121 | @$(OCI) run \ 122 | -w /root \ 123 | --hostname testhost \ 124 | --rm -it \ 125 | -v "$(CURDIR)/.testyadm:/bin/yadm:ro" \ 126 | -v "$(CURDIR)/completion/bash/yadm:/usr/share/bash-completion/completions/yadm:ro" \ 127 | $(IMAGE) \ 128 | bash -l 129 | 130 | .PHONY: scripthost 131 | scripthost: require-docker .testyadm 132 | @echo "Starting scripthost \(recording script\)" 133 | @printf '' > script.gz 134 | @$(OCI) run \ 135 | -w /root \ 136 | --hostname scripthost \ 137 | --rm -it \ 138 | -v "$(CURDIR)/script.gz:/script.gz:rw" \ 139 | -v "$(CURDIR)/.testyadm:/bin/yadm:ro" \ 140 | $(IMAGE) \ 141 | bash -c "script /tmp/script -q -c 'bash -l'; gzip < /tmp/script > /script.gz" 142 | @echo 143 | @echo "Script saved to $(CURDIR)/script.gz" 144 | 145 | 146 | .PHONY: testenv 147 | testenv: 148 | @echo 'Creating a local virtual environment in "testenv/"' 149 | @echo 150 | @rm -rf testenv 151 | python3 -m venv --clear testenv 152 | testenv/bin/pip3 install --upgrade pip setuptools 153 | testenv/bin/pip3 install --upgrade -r test/requirements.txt; 154 | @for v in $$(sed -En -e 's:.*/yadm-([0-9.]+)$$:\1:p' test/Dockerfile); do \ 155 | git show $$v:yadm > testenv/bin/yadm-$$v; \ 156 | chmod +x testenv/bin/yadm-$$v; \ 157 | done 158 | @echo 159 | @echo 'To activate this test environment type:' 160 | @echo ' source testenv/bin/activate' 161 | 162 | .PHONY: image 163 | image: 164 | @$(OCI) build -f test/Dockerfile . -t "$(IMAGE)" 165 | 166 | 167 | .PHONY: man 168 | man: 169 | @groff -man -Tascii ./yadm.1 | less 170 | 171 | .PHONY: man-wide 172 | man-wide: 173 | @man ./yadm.1 174 | 175 | .PHONY: man-ps 176 | man-ps: 177 | @groff -man -Tps ./yadm.1 > yadm.ps 178 | 179 | yadm.md: yadm.1 180 | @groff -man -Tutf8 -Z ./yadm.1 | grotty -c | col -bx | sed 's/^[A-Z]/## &/g' | sed '/YADM(1)/d' > yadm.md 181 | 182 | .PHONY: contrib 183 | contrib: SHELL = /bin/bash 184 | contrib: 185 | @echo -e "CONTRIBUTORS\n" > CONTRIBUTORS 186 | @IFS=$$'\n'; for author in $$(git shortlog -ns master gh-pages develop dev-pages | cut -f2); do \ 187 | git log master gh-pages develop dev-pages \ 188 | --author="$$author" --format=tformat: --numstat | \ 189 | awk "{sum += \$$1 + \$$2} END {print sum \"\t\" \"$$author\"}"; \ 190 | done | sort -nr | cut -f2 >> CONTRIBUTORS 191 | 192 | .PHONY: install 193 | install: 194 | @[ -n "$(PREFIX)" ] || { echo "PREFIX is not set"; exit 1; } 195 | @{\ 196 | set -e ;\ 197 | bin="$(DESTDIR)$(PREFIX)/bin" ;\ 198 | doc="$(DESTDIR)$(PREFIX)/share/doc/yadm" ;\ 199 | man="$(DESTDIR)$(PREFIX)/share/man/man1" ;\ 200 | install -d "$$bin" "$$doc" "$$man" ;\ 201 | install -m 0755 yadm "$$bin" ;\ 202 | install -m 0644 yadm.1 "$$man" ;\ 203 | install -m 0644 CHANGES CONTRIBUTORS LICENSE "$$doc" ;\ 204 | cp -r contrib "$$doc" ;\ 205 | } 206 | 207 | .PHONY: sync-clock 208 | sync-clock: 209 | $(OCI) run --rm --privileged alpine hwclock -s 210 | 211 | .PHONY: require-docker 212 | require-docker: 213 | @if ! command -v $(OCI) > /dev/null 2>&1; then \ 214 | echo "Sorry, this make target requires docker to be installed, to use another docker-compatible engine, like podman, re-run the make command adding OCI=podman"; \ 215 | false; \ 216 | fi 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yadm - Yet Another Dotfiles Manager 2 | 3 | [![Latest Version][releases-badge]][releases-link] 4 | [![Homebrew Version][homebrew-badge]][homebrew-link] 5 | [![OBS Version][obs-badge]][obs-link] 6 | [![Arch Version][arch-badge]][arch-link] 7 | [![License][license-badge]][license-link]
8 | [![Master Update][master-date]][master-commits] 9 | [![Develop Update][develop-date]][develop-commits] 10 | [![Website Update][website-date]][website-commits]
11 | [![Master Status][master-badge]][workflow-master] 12 | [![Develop Status][develop-badge]][workflow-develop] 13 | [![GH Pages Status][gh-pages-badge]][workflow-gh-pages] 14 | [![Dev Pages Status][dev-pages-badge]][workflow-dev-pages] 15 | 16 | [https://yadm.io/][website-link] 17 | 18 | **yadm** is a tool for managing [dotfiles][]. 19 | 20 | * Based on [Git][], with full range of Git's features 21 | * Supports system-specific [alternative][feature-alternates] files or 22 | [templated][feature-templates] files 23 | * [Encryption][feature-encryption] of private data using [GnuPG][], 24 | [OpenSSL][], [transcrypt][], or [git-crypt][] 25 | * Customizable initialization ([bootstrapping][feature-bootstrap]) 26 | * Customizable [hooks][feature-hooks] for before and after any operation 27 | 28 | Complete features, usage, examples and installation instructions can be found on 29 | the [yadm.io][website-link] website. 30 | 31 | ## A very quick tour 32 | 33 | # Initialize a new repository 34 | yadm init 35 | 36 | # Clone an existing repository 37 | yadm clone 38 | 39 | # Add files/changes 40 | yadm add 41 | yadm commit 42 | 43 | # Encrypt your ssh key 44 | echo '.ssh/id_rsa' > ~/.config/yadm/encrypt 45 | yadm encrypt 46 | 47 | # Later, decrypt your ssh key 48 | yadm decrypt 49 | 50 | # Create different files for Linux vs MacOS 51 | yadm add path/file.cfg##os.Linux 52 | yadm add path/file.cfg##os.Darwin 53 | 54 | If you enjoy using yadm, consider adding a star to the repository on GitHub. 55 | The star count helps others discover yadm. 56 | 57 | [Git]: https://git-scm.com/ 58 | [GnuPG]: https://gnupg.org/ 59 | [OpenSSL]: https://www.openssl.org/ 60 | [arch-badge]: https://img.shields.io/archlinux/v/extra/any/yadm 61 | [arch-link]: https://archlinux.org/packages/extra/any/yadm/ 62 | [dev-pages-badge]: https://img.shields.io/github/actions/workflow/status/yadm-dev/yadm/test.yml?branch=dev-pages 63 | [develop-badge]: https://img.shields.io/github/actions/workflow/status/yadm-dev/yadm/test.yml?branch=develop 64 | [develop-commits]: https://github.com/yadm-dev/yadm/commits/develop 65 | [develop-date]: https://img.shields.io/github/last-commit/yadm-dev/yadm/develop.svg?label=develop 66 | [dotfiles]: https://en.wikipedia.org/wiki/Hidden_file_and_hidden_directory 67 | [feature-alternates]: https://yadm.io/docs/alternates 68 | [feature-bootstrap]: https://yadm.io/docs/bootstrap 69 | [feature-hooks]: https://yadm.io/docs/hooks 70 | [feature-encryption]: https://yadm.io/docs/encryption 71 | [feature-templates]: https://yadm.io/docs/templates 72 | [gh-pages-badge]: https://img.shields.io/github/actions/workflow/status/yadm-dev/yadm/test.yml?branch=gh-pages 73 | [git-crypt]: https://github.com/AGWA/git-crypt 74 | [homebrew-badge]: https://img.shields.io/homebrew/v/yadm.svg 75 | [homebrew-link]: https://formulae.brew.sh/formula/yadm 76 | [license-badge]: https://img.shields.io/github/license/yadm-dev/yadm.svg 77 | [license-link]: https://github.com/yadm-dev/yadm/blob/master/LICENSE 78 | [master-badge]: https://img.shields.io/github/actions/workflow/status/yadm-dev/yadm/test.yml?branch=master 79 | [master-commits]: https://github.com/yadm-dev/yadm/commits/master 80 | [master-date]: https://img.shields.io/github/last-commit/yadm-dev/yadm/master.svg?label=master 81 | [obs-badge]: https://img.shields.io/badge/OBS-v3.5.0-blue 82 | [obs-link]: https://software.opensuse.org/download.html?project=home%3ATheLocehiliosan%3Ayadm&package=yadm 83 | [releases-badge]: https://img.shields.io/github/tag/yadm-dev/yadm.svg?label=latest+release 84 | [releases-link]: https://github.com/yadm-dev/yadm/releases 85 | [transcrypt]: https://github.com/elasticdog/transcrypt 86 | [website-commits]: https://github.com/yadm-dev/yadm/commits/gh-pages 87 | [website-date]: https://img.shields.io/github/last-commit/yadm-dev/yadm/gh-pages.svg?label=website 88 | [website-link]: https://yadm.io/ 89 | [workflow-dev-pages]: https://github.com/yadm-dev/yadm/actions?query=workflow%3a%22test+site%22+branch%3adev-pages 90 | [workflow-develop]: https://github.com/yadm-dev/yadm/actions?query=workflow%3ATests+branch%3Adevelop 91 | [workflow-gh-pages]: https://github.com/yadm-dev/yadm/actions?query=workflow%3a%22test+site%22+branch%3agh-pages 92 | [workflow-master]: https://github.com/yadm-dev/yadm/actions?query=workflow%3ATests+branch%3Amaster 93 | -------------------------------------------------------------------------------- /bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # This script can be "curl-piped" into bash to bootstrap a dotfiles repo when 5 | # yadm is not locally installed. Read below for instructions. 6 | # 7 | # This script is hosted at bootstrap.yadm.io to make it easy to remember/type. 8 | # 9 | # DISCLAIMER: In general, I would advise against piping someone's code directly 10 | # from the Internet into an interpreter (like Bash). You should 11 | # probably review any code like this prior to executing it. I leave 12 | # it to you to decide if this is risky behavior or not. The main 13 | # reason this script exists is because I find it to be a pragmatic 14 | # way to bootstrap my dotfiles, and install yadm in one step 15 | # (allowing the yadm project to be a submodule of my dotfiles 16 | # repo). 17 | # 18 | # Invoke bootstrap with: 19 | # 20 | # curl -L bootstrap.yadm.io | bash 21 | # 22 | # OR 23 | # 24 | # curl -L bootstrap.yadm.io | bash [-s -- REPO_URL [YADM_RELEASE]] 25 | # 26 | # Alternatively, source in this file to export a yadm() function which uses 27 | # yadm remotely until it is locally installed. 28 | # 29 | # source <(curl -L bootstrap.yadm.io) 30 | # 31 | 32 | YADM_REPO="https://github.com/yadm-dev/yadm" 33 | YADM_RELEASE=${release:-master} 34 | REPO_URL="" 35 | 36 | function _private_yadm() { 37 | unset -f yadm 38 | if command -v yadm &>/dev/null; then 39 | echo "Found yadm installed locally, removing remote yadm() function" 40 | unset -f _private_yadm 41 | command yadm "$@" 42 | else 43 | function yadm() { _private_yadm "$@"; } 44 | export -f yadm 45 | echo WARNING: Using yadm remotely. You should install yadm locally. 46 | curl -fsSL "$YADM_REPO/raw/$YADM_RELEASE/yadm" | bash -s -- "$@" 47 | fi 48 | } 49 | export -f _private_yadm 50 | function yadm() { _private_yadm "$@"; } 51 | export -f yadm 52 | 53 | # if being sourced, return here, otherwise continue processing 54 | return 2>/dev/null 55 | unset -f yadm 56 | 57 | function remote_yadm() { 58 | curl -fsSL "$YADM_REPO/raw/$YADM_RELEASE/yadm" | bash -s -- "$@" 59 | } 60 | 61 | function ask_about_source() { 62 | if ! command -v yadm &>/dev/null; then 63 | echo 64 | echo "***************************************************" 65 | echo "yadm is NOT currently installed." 66 | echo "You should install it locally, this link may help:" 67 | echo "https://yadm.io/docs/install" 68 | echo "***************************************************" 69 | echo 70 | echo "If installation is not possible right now, you can temporarily \"source\"" 71 | echo "in a yadm() function which fetches yadm remotely each time it is called." 72 | echo 73 | echo " source <(curl -L bootstrap.yadm.io)" 74 | echo 75 | fi 76 | } 77 | 78 | function build_url() { 79 | echo "No repo URL provided." 80 | echo 81 | echo "Where is your repo?" 82 | echo 83 | echo " 1. GitHub" 84 | echo " 2. Bitbucket" 85 | echo " 3. GitLab" 86 | echo " 4. Other" 87 | echo 88 | read -r -p "Where is your repo? (1/2/3/4) ->" choice " choice " choice /dev/null && ! declare -F __git_wrap__git_main >/dev/null; then 3 | if declare -F _completion_loader >/dev/null; then 4 | _completion_loader git 5 | fi 6 | fi 7 | 8 | # only operate if git completion is present 9 | if declare -F _git >/dev/null || declare -F __git_wrap__git_main >/dev/null; then 10 | 11 | _yadm() { 12 | 13 | local current=${COMP_WORDS[COMP_CWORD]} 14 | local penultimate 15 | if ((COMP_CWORD >= 1)); then 16 | penultimate=${COMP_WORDS[COMP_CWORD - 1]} 17 | fi 18 | local antepenultimate 19 | if ((COMP_CWORD >= 2)); then 20 | antepenultimate=${COMP_WORDS[COMP_CWORD - 2]} 21 | fi 22 | 23 | local -x GIT_DIR 24 | GIT_DIR="$(yadm introspect repo 2>/dev/null)" 25 | 26 | case "$penultimate" in 27 | bootstrap) 28 | COMPREPLY=() 29 | return 0 30 | ;; 31 | config) 32 | COMPREPLY=($(compgen -W "$(yadm introspect configs 2>/dev/null)")) 33 | return 0 34 | ;; 35 | decrypt) 36 | COMPREPLY=($(compgen -W "-l" -- "$current")) 37 | return 0 38 | ;; 39 | init) 40 | COMPREPLY=($(compgen -W "-f -w" -- "$current")) 41 | return 0 42 | ;; 43 | introspect) 44 | COMPREPLY=($(compgen -W "commands configs repo switches" -- "$current")) 45 | return 0 46 | ;; 47 | help) 48 | COMPREPLY=() # no specific help yet 49 | return 0 50 | ;; 51 | list) 52 | COMPREPLY=($(compgen -W "-a" -- "$current")) 53 | return 0 54 | ;; 55 | esac 56 | 57 | case "$antepenultimate" in 58 | clone) 59 | COMPREPLY=($(compgen -W "-f -w -b --bootstrap --no-bootstrap" -- "$current")) 60 | return 0 61 | ;; 62 | esac 63 | 64 | local yadm_switches=($(yadm introspect switches 2>/dev/null)) 65 | 66 | # this condition is so files are completed properly for --yadm-xxx options 67 | if [[ " ${yadm_switches[*]} " != *" $penultimate "* ]]; then 68 | # TODO: somehow solve the problem with [--yadm-xxx option] being 69 | # incompatible with what git expects, namely [--arg=option] 70 | if declare -F _git >/dev/null; then 71 | _git 72 | else 73 | __git_wrap__git_main 74 | fi 75 | fi 76 | if [[ "$current" =~ ^- ]]; then 77 | __gitcompappend "${yadm_switches[*]}" "" "$current" " " 78 | fi 79 | 80 | # Find the index of where the sub-command argument should go. 81 | local command_idx 82 | for ((command_idx = 1; command_idx < ${#COMP_WORDS[@]}; command_idx++)); do 83 | local command_idx_arg="${COMP_WORDS[$command_idx]}" 84 | if [[ " ${yadm_switches[*]} " = *" $command_idx_arg "* ]]; then 85 | let command_idx++ 86 | elif [[ "$command_idx_arg" = -* ]]; then 87 | : 88 | else 89 | break 90 | fi 91 | done 92 | if [[ "$COMP_CWORD" = "$command_idx" ]]; then 93 | __gitcompappend "$(yadm introspect commands 2>/dev/null)" "" "$current" " " 94 | fi 95 | } 96 | 97 | complete -o bashdefault -o default -o nospace -F _yadm yadm 2>/dev/null || 98 | complete -o default -o nospace -F _yadm yadm 99 | 100 | fi 101 | -------------------------------------------------------------------------------- /completion/fish/yadm.fish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/fish 2 | 3 | function __fish_yadm_universial_optspecs 4 | string join \n 'a-yadm-dir=' 'b-yadm-repo=' 'c-yadm-config=' \ 5 | 'd-yadm-encrypt=' 'e-yadm-archive=' 'f-yadm-bootstrap=' 6 | end 7 | 8 | function __fish_yadm_needs_command 9 | # Figure out if the current invocation already has a command. 10 | set -l cmd (commandline -opc) 11 | set -e cmd[1] 12 | argparse -s (__fish_yadm_universial_optspecs) -- $cmd 2>/dev/null 13 | or return 0 14 | if set -q argv[1] 15 | echo $argv[1] 16 | return 1 17 | end 18 | return 0 19 | end 20 | 21 | function __fish_yadm_using_command 22 | set -l cmd (__fish_yadm_needs_command) 23 | test -z "$cmd" 24 | and return 1 25 | contains -- $cmd $argv 26 | and return 0 27 | end 28 | 29 | # yadm's specific autocomplete 30 | complete -x -c yadm -n '__fish_yadm_needs_command' -a 'clone' -d 'Clone an existing repository' 31 | complete -F -c yadm -n '__fish_yadm_using_command clone' -s w -d 'work-tree to use (default: $HOME)' 32 | complete -f -c yadm -n '__fish_yadm_using_command clone' -s b -d 'branch to clone' 33 | complete -x -c yadm -n '__fish_yadm_using_command clone' -s f -d 'force to overwrite' 34 | complete -x -c yadm -n '__fish_yadm_using_command clone' -l bootstrap -d 'force bootstrap to run' 35 | complete -x -c yadm -n '__fish_yadm_using_command clone' -l no-bootstrap -d 'prevent bootstrap from beingrun' 36 | 37 | complete -x -c yadm -n '__fish_yadm_needs_command' -a 'alt' -d 'Create links for alternates' 38 | complete -x -c yadm -n '__fish_yadm_needs_command' -a 'bootstrap' -d 'Execute $HOME/.config/yadm/bootstrap' 39 | complete -x -c yadm -n '__fish_yadm_needs_command' -a 'perms' -d 'Fix perms for private files' 40 | complete -x -c yadm -n '__fish_yadm_needs_command' -a 'enter' -d 'Run sub-shell with GIT variables set' 41 | complete -c yadm -n '__fish_yadm_needs_command' -a 'git-crypt' -d 'Run git-crypt commands for the yadm repo' 42 | complete -x -c yadm -n '__fish_yadm_needs_command' -a 'help' -d 'Print a summary of yadm commands' 43 | complete -x -c yadm -n '__fish_yadm_needs_command' -a 'upgrade' -d 'Upgrade to version 2 of yadm directory structure' 44 | complete -x -c yadm -n '__fish_yadm_needs_command' -a 'version' -d 'Print the version of yadm' 45 | 46 | complete -x -c yadm -n '__fish_yadm_needs_command' -a 'init' -d 'Initialize an empty repository' 47 | complete -x -c yadm -n '__fish_yadm_using_command init' -s f -d 'force to overwrite' 48 | complete -F -c yadm -n '__fish_yadm_using_command init' -s w -d 'set work-tree (default: $HOME)' 49 | 50 | complete -x -c yadm -n '__fish_yadm_needs_command' -a 'list' -d 'List tracked files at current directory' 51 | complete -x -c yadm -n '__fish_yadm_using_command list' -s a -d 'list all managed files instead' 52 | 53 | complete -x -c yadm -n '__fish_yadm_needs_command' -a 'encrypt' -d 'Encrypt files' 54 | complete -x -c yadm -n '__fish_yadm_needs_command' -a 'decrypt' -d 'Decrypt files' 55 | complete -x -c yadm -n '__fish_yadm_using_command decrypt' -s l -d 'list the files stored without extracting' 56 | 57 | complete -x -c yadm -n '__fish_yadm_needs_command' -a 'introspect' -d 'Report internal yadm data' 58 | complete -x -c yadm -n '__fish_yadm_using_command introspect' -a (printf -- '%s\n' 'commands configs repo switches') -d 'category' 59 | 60 | complete -x -c yadm -n '__fish_yadm_needs_command' -a 'gitconfig' -d 'Pass options to the git config command' 61 | complete -x -c yadm -n '__fish_yadm_needs_command' -a 'config' -d 'Configure a setting' 62 | for name in (yadm introspect configs) 63 | complete -x -c yadm -n '__fish_yadm_using_command config' -a $name -d 'yadm config' 64 | end 65 | 66 | # yadm universial options 67 | complete --force-files -c yadm -s Y -l yadm-dir -d 'Override location of yadm directory' 68 | complete --force-files -c yadm -l yadm-repo -d 'Override location of yadm repository' 69 | complete --force-files -c yadm -l yadm-config -d 'Override location of yadm configuration file' 70 | complete --force-files -c yadm -l yadm-encrypt -d 'Override location of yadm encryption configuration' 71 | complete --force-files -c yadm -l yadm-archive -d 'Override location of yadm encrypted files archive' 72 | complete --force-files -c yadm -l yadm-bootstrap -d 'Override location of yadm bootstrap program' 73 | 74 | # wraps git's autocomplete 75 | set -l GIT_DIR (yadm introspect repo) 76 | # setup the correct git-dir by appending it to git's argunment 77 | complete -c yadm -w "git --git-dir=$GIT_DIR" 78 | -------------------------------------------------------------------------------- /completion/zsh/_yadm: -------------------------------------------------------------------------------- 1 | #compdef yadm 2 | 3 | # This completion tries to fallback to git's completion for git commands. 4 | 5 | zstyle -T ':completion:*:yadm:argument-1:descriptions:' format && \ 6 | zstyle ':completion:*:yadm:argument-1:descriptions' format '%d:' 7 | zstyle -T ':completion:*:yadm:*:yadm' group-name && \ 8 | zstyle ':completion:*:yadm:*:yadm' group-name '' 9 | 10 | function _yadm-add(){ 11 | local -a yadm_options yadm_path 12 | yadm_path="$(yadm rev-parse --show-toplevel)" 13 | yadm_options=($(yadm status --porcelain=v1 | 14 | awk -v yadm_path=${yadm_path} '{printf "%s/%s:%s\n", yadm_path, $2, $1}' )) 15 | 16 | _describe 'command' yadm_options 17 | _files 18 | } 19 | 20 | function _yadm-checkout(){ 21 | _yadm-add 22 | } 23 | 24 | _yadm-alt() { 25 | return 0 26 | } 27 | 28 | _yadm-bootstrap() { 29 | return 0 30 | } 31 | 32 | _yadm-clone() { 33 | _arguments \ 34 | '(--bootstrap --no-bootstrap)--bootstrap[force bootstrap, without prompt]' \ 35 | '(--bootstrap --no-bootstrap)--no-bootstrap[prevent bootstrap, without prompt]' \ 36 | '-f[force overwrite of existing repository]' \ 37 | '-w[yadm work tree path]: :_files -/' 38 | 39 | local curcontext="${curcontext%:*:*}:git:" 40 | 41 | words=("git" "${words[@]}") CURRENT=$((CURRENT + 1)) service=git _git 42 | } 43 | 44 | _yadm-config() { 45 | # TODO: complete config names 46 | } 47 | 48 | _yadm-decrypt() { 49 | _arguments \ 50 | '-l[list files]' 51 | } 52 | 53 | _yadm-encrypt() { 54 | return 0 55 | } 56 | 57 | _yadm-enter() { 58 | _arguments \ 59 | ':command: _command_names -e' \ 60 | '*::arguments: _normal' 61 | } 62 | 63 | _yadm-git-crypt() { 64 | # TODO: complete git-crypt options 65 | } 66 | 67 | _yadm-help() { 68 | return 0 69 | } 70 | 71 | _yadm-init() { 72 | _arguments \ 73 | '-f[force overwrite of existing repository]' \ 74 | '-w[work tree path]: :_files -/' 75 | } 76 | 77 | _yadm-list() { 78 | _arguments \ 79 | '-a[list all tracked files]' 80 | } 81 | 82 | _yadm-perms() { 83 | return 0 84 | } 85 | 86 | _yadm-transcrypt() { 87 | integer _ret=1 88 | _call_function _ret _transcrypt 89 | return _ret 90 | } 91 | 92 | _yadm-upgrade() { 93 | _arguments \ 94 | '-f[force deinit of submodules]' \ 95 | ': ' 96 | } 97 | 98 | _yadm-version() { 99 | return 0 100 | } 101 | 102 | _yadm_commands() { 103 | local -a commands=( 104 | alt:'create links for alternates' 105 | bootstrap:'execute bootstrap' 106 | clone:'clone an existing yadm repository' 107 | config:'configure an yadm setting' 108 | decrypt:'decrypt files' 109 | encrypt:'encrypt files' 110 | enter:'run sub-shell with GIT variables set' 111 | git-crypt:'run git-crypt commands for the yadm repository' 112 | gitconfig:'run the git config command' 113 | help:'display yadm help information' 114 | init:'initialize an empty yadm repository' 115 | list:'list files tracked by yadm' 116 | perms:'fix perms for private files' 117 | transcrypt:'run transcrypt commands for the yadm repository' 118 | upgrade:'upgrade legacy yadm paths' 119 | version:'show yadm version' 120 | ) 121 | 122 | local oldcontext="$curcontext" 123 | local curcontext="${curcontext%:*:*}:git:" 124 | 125 | words=("git" "${words[-1]}") CURRENT=2 service=git _git 126 | 127 | curcontext="$oldcontext" 128 | _describe -t yadm "yadm commands" commands 129 | 130 | return 0 131 | } 132 | 133 | _yadm() { 134 | local curcontext=$curcontext state state_descr line 135 | declare -A opt_args 136 | 137 | _arguments -C \ 138 | '(-Y --yadm-dir)'{-Y,--yadm-dir}'[override the standard yadm directory]: :_files -/' \ 139 | '--yadm-data[override the standard yadm data directory]: :_files -/' \ 140 | '--yadm-repo[override the standard repo path]: :_files -/' \ 141 | '--yadm-config[override the standard config path]: :_files -/' \ 142 | '--yadm-encrypt[override the standard encrypt path]: :_files -/' \ 143 | '--yadm-archive[override the standard archive path]: :_files -/' \ 144 | '--yadm-bootstrap[override the standard bootstrap path]: :_files' \ 145 | '--help[display yadm help information]' \ 146 | '--version[show yadm version]' \ 147 | '(-): :->command' \ 148 | '(-)*:: :->option-or-argument' && return 149 | 150 | local -a repo_args 151 | (( $+opt_args[--yadm-repo] )) && repo_args+=(--yadm-repo "$opt_args[--yadm-repo]") 152 | (( $+opt_args[--yadm-data] )) && repo_args+=(--yadm-data "$opt_args[--yadm-data]") 153 | local -x GIT_DIR="$(_call_program gitdir yadm "${repo_args[@]}" introspect repo)" 154 | [[ -z "$GIT_DIR" ]] && return 1 155 | 156 | integer _ret=1 157 | case $state in 158 | (command) 159 | _yadm_commands && _ret=0 160 | ;; 161 | (option-or-argument) 162 | curcontext=${curcontext%:*:*}:yadm-${words[1]}: 163 | if ! _call_function _ret _yadm-${words[1]}; then 164 | 165 | # Translate gitconfig to use the regular completion for config 166 | [[ ${words[1]} = "gitconfig" ]] && words[1]=config 167 | 168 | words=("git" "${(@)words}") 169 | CURRENT=$(( CURRENT + 1 )) 170 | 171 | curcontext=${curcontext%:*:*}:git: 172 | service=git _git && _ret=0 173 | fi 174 | ;; 175 | esac 176 | 177 | return _ret 178 | } 179 | 180 | (( $+functions[_git] )) && _yadm 181 | -------------------------------------------------------------------------------- /contrib/bootstrap/bootstrap-in-dir: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Save this file as ~/.config/yadm/bootstrap and make it executable. It will 4 | # execute all executable files (excluding templates and editor backups) in the 5 | # ~/.config/yadm/bootstrap.d directory when run. 6 | 7 | set -eu 8 | 9 | # Directory to look for bootstrap executables in 10 | BOOTSTRAP_D="${BASH_SOURCE[0]}.d" 11 | 12 | if [[ ! -d "$BOOTSTRAP_D" ]]; then 13 | echo "Error: bootstrap directory '$BOOTSTRAP_D' not found" >&2 14 | exit 1 15 | fi 16 | 17 | declare -a bootstraps 18 | while IFS= read -r bootstrap; do 19 | if [[ -x "$bootstrap" && ! "$bootstrap" =~ "##" && ! "$bootstrap" =~ ~$ ]]; then 20 | bootstraps+=("$bootstrap") 21 | fi 22 | done < <(find -L "$BOOTSTRAP_D" -type f | sort) 23 | 24 | for bootstrap in "${bootstraps[@]}"; do 25 | if ! "$bootstrap"; then 26 | echo "Error: bootstrap '$bootstrap' failed" >&2 27 | exit 1 28 | fi 29 | done 30 | -------------------------------------------------------------------------------- /contrib/commands/README.md: -------------------------------------------------------------------------------- 1 | ## Contributed Commands 2 | 3 | Although these commands are available as part of the official 4 | **yadm** source tree, they have a somewhat different status. The intention is to 5 | keep interesting and potentially useful commands here, building a library of 6 | examples that might help others. 7 | 8 | I recommend *careful review* of any code from here before using it. No 9 | guarantees of code quality is assumed. 10 | -------------------------------------------------------------------------------- /contrib/commands/yadm-untracked: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # To run: `yadm-untracked ` 4 | # 5 | # If you wish to create a YADM alias to run this as, for example `yadm untracked` 6 | # then the following command will add the alias: 7 | # `yadm gitconfig alias.untracked '!/yadm-untracked'` 8 | 9 | # Possible script improvements: 10 | # - Reduce the amount of configuration; I have not figured out a way to 11 | # get rid of the non-recursive and ignore. The recursive list could be 12 | # built from the directories that are present in `yadm list` 13 | 14 | # Configuration... The script looks at the following 3 arrays: 15 | # 16 | # yadm_tracked_recursively 17 | # The directories and files in this list are searched recursively to build 18 | # a list of files that you expect are tracked with `yadm`. Items in this 19 | # list are relative to the root of your YADM repo (which is $HOME for most). 20 | 21 | # yadm_tracked_nonrecursively 22 | # Same as above but don't search recursively 23 | # 24 | # ignore_files_and_dirs 25 | # A list of directories and files that will not be reported as untracked if 26 | # found in the above two searches. 27 | # 28 | # Example configuration file (uncomment it to use): 29 | # yadm_tracked_recursively=( 30 | # bin .config .vim 31 | # ) 32 | # 33 | # yadm_tracked_nonrecursively=( 34 | # ~ 35 | # ) 36 | # 37 | # ignore_files_and_dirs=( 38 | # .CFUserTextEncoding .DS_Store .config/gh 39 | # .vim/autoload/plug.vim 40 | # ) 41 | 42 | if [[ $# -ne 1 ]]; then 43 | echo 'Usage: yadm-untracked ' 44 | exit 1 45 | fi 46 | 47 | yadm_tracked_recursively=() 48 | yadm_tracked_nonrecursively=() 49 | ignore_files_and_dirs=() 50 | 51 | source $1 52 | 53 | root=`yadm enter echo '$GIT_WORK_TREE'` 54 | 55 | cd $root 56 | 57 | find_list=$(mktemp -t find_list) 58 | find ${yadm_tracked_recursively[*]} -type f >$find_list 59 | find ${yadm_tracked_nonrecursively[*]} -maxdepth 1 -type f | 60 | awk "{sub(\"^\./\", \"\"); sub(\"^$root/\", \"\"); print }" >>$find_list 61 | sort -o $find_list $find_list 62 | 63 | yadm_list=$(mktemp -t yadm_list) 64 | yadm list >$yadm_list 65 | find ${ignore_files_and_dirs[*]} -type f >>$yadm_list 66 | sort -o $yadm_list $yadm_list 67 | 68 | # Show the files not in `yadm list` 69 | comm -23 $find_list $yadm_list 70 | 71 | rm -f $find_list $yadm_list 72 | -------------------------------------------------------------------------------- /contrib/hooks/README.md: -------------------------------------------------------------------------------- 1 | ## Contributed Hooks 2 | 3 | Although these [hooks][hooks-help] are available as part of the official 4 | **yadm** source tree, they have a somewhat different status. The intention is to 5 | keep interesting and potentially useful hooks here, building a library of 6 | examples that might help others. 7 | 8 | In some cases, an experimental new feature can be build entirely with hooks, and 9 | this is a place to share it. 10 | 11 | I recommend *careful review* of any code from here before using it. No 12 | guarantees of code quality is assumed. 13 | 14 | [hooks-help]: https://github.com/yadm-dev/yadm/blob/master/yadm.md#hooks 15 | -------------------------------------------------------------------------------- /contrib/hooks/encrypt_with_checksums/README.md: -------------------------------------------------------------------------------- 1 | ## Track checksums of encrypted files 2 | 3 | Contributed by Martin Zuther 4 | 5 | Hook | Description 6 | ---- | ----------- 7 | post_encrypt | Collects the checksums of encrypted files, and stores them in .config/yadm/files.checksums 8 | post_list | Prints the names of encrypted files 9 | post_status | Reports untracked changes within encrypted files 10 | -------------------------------------------------------------------------------- /contrib/hooks/encrypt_with_checksums/post_encrypt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # yadm - Yet Another Dotfiles Manager 4 | # Copyright (C) 2015-2021 Tim Byrne and Martin Zuther 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | 20 | YADM_CHECKSUMS="$YADM_HOOK_DIR/files.checksums" 21 | WARNING_MESSAGE="No checksums were created" 22 | 23 | # unpack exported array; filenames including a newline character (\n) 24 | # are NOT supported 25 | OLD_IFS="$IFS" 26 | IFS=$'\n' 27 | YADM_ENCRYPT_INCLUDE_FILES=( $YADM_ENCRYPT_INCLUDE_FILES ) 28 | IFS="$OLD_IFS" 29 | 30 | 31 | function get_checksum_command { 32 | # check if "shasum" exists and supports the algorithm (which is 33 | # tested by sending an empty string to "shasum") 34 | if command -v "shasum" > /dev/null && printf "" | shasum --algorithm "256" &> /dev/null; then 35 | printf "shasum --algorithm 256" 36 | # check if "sha256sum" exists 37 | elif command -v "sha256sum" > /dev/null; then 38 | printf "sha256sum" 39 | # check if "gsha256sum" exists 40 | elif command -v "gsha256sum" > /dev/null; then 41 | printf "gsha256sum" 42 | else 43 | # display warning in bright yellow 44 | printf "\033[1;33m" >&2 45 | printf "\nWARNING: \"shasum\", \"sha256sum\" and \"gsha256sum\" not found. %s\n" "$WARNING_MESSAGE." >&2 46 | 47 | # reset output color 48 | printf "\033[0m" >&2 49 | 50 | # signal error 51 | return 1 52 | fi 53 | } 54 | 55 | 56 | # get checksum command 57 | CHECKSUM_COMMAND=$(get_checksum_command) 58 | 59 | # no command found 60 | if (($?)); then 61 | # return original exit status of yadm command 62 | exit "$YADM_HOOK_EXIT" 63 | fi 64 | 65 | # empty (or create) checksum file 66 | true > "$YADM_CHECKSUMS" 67 | 68 | # calculate checksums for encrypted files 69 | for included in "${YADM_ENCRYPT_INCLUDE_FILES[@]}"; do 70 | # highlight any errors in red 71 | printf "\033[0;31m" 72 | 73 | # calculate checksums 74 | $CHECKSUM_COMMAND "$included" >> "$YADM_CHECKSUMS" 75 | ERROR_CODE=$? 76 | 77 | # reset output color 78 | printf "\033[0m" 79 | 80 | # handle errors 81 | if (($ERROR_CODE)); then 82 | # display warning in bright yellow 83 | printf "\033[1;33m" >&2 84 | printf "\nWARNING: an error occurred. Please inspect the checksum file.\n" >&2 85 | 86 | # reset output color 87 | printf "\033[0m" >&2 88 | 89 | # exit and signal error 90 | exit $ERROR_CODE 91 | fi 92 | done 93 | 94 | # announce success and return original exit status of yadm command 95 | printf "Wrote SHA-256 checksums: %s\n" "$YADM_CHECKSUMS" 96 | exit "$YADM_HOOK_EXIT" 97 | -------------------------------------------------------------------------------- /contrib/hooks/encrypt_with_checksums/post_list: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # yadm - Yet Another Dotfiles Manager 4 | # Copyright (C) 2015-2021 Tim Byrne and Martin Zuther 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | YADM_CHECKSUMS="$YADM_HOOK_DIR/files.checksums" 20 | 21 | 22 | # is current directory on yadm's work path? 23 | # (adapted from https://unix.stackexchange.com/a/6438/122163) 24 | if [ "${PWD##$YADM_HOOK_WORK}" != "$PWD" ]; then 25 | ON_WORK_PATH=1 26 | else 27 | ON_WORK_PATH=0 28 | fi 29 | 30 | 31 | # list all files or only those in the subdirectories below? 32 | OPTION_LIST_ALL=0 33 | for argument in "${YADM_HOOK_FULL_COMMAND[@]}"; do 34 | # mimick git ls-files by displaying all files when not on work 35 | # path 36 | if [ "$argument" = "-a" ] || [ $ON_WORK_PATH -eq 0 ]; then 37 | OPTION_LIST_ALL=1 38 | break 39 | fi 40 | done 41 | 42 | 43 | # if there is no checksum file, exit with original status of yadm 44 | # command 45 | if [ ! -f "$YADM_CHECKSUMS" ]; then 46 | exit "$YADM_HOOK_EXIT" 47 | fi 48 | 49 | # list encrypted files 50 | while IFS= read -r filename; do 51 | # remove checksums from file names 52 | filename="${filename##[a-zA-Z0-9]* }" 53 | 54 | # list only files in the subdirectories below (i.e. files 55 | # whose relative path doesn't begin with "../") 56 | if [ $OPTION_LIST_ALL -eq 0 ]; then 57 | REL_PATH=$(relative_path "$PWD" "$YADM_HOOK_WORK/$filename") 58 | 59 | if [ "$REL_PATH" = "${REL_PATH##../}" ]; then 60 | printf "%s\n" "$REL_PATH" 61 | fi 62 | # list all files 63 | else 64 | printf "%s\n" "$filename" 65 | fi 66 | done < "$YADM_CHECKSUMS" 67 | 68 | # return original exit status of yadm command 69 | exit "$YADM_HOOK_EXIT" 70 | -------------------------------------------------------------------------------- /contrib/hooks/encrypt_with_checksums/post_status: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # yadm - Yet Another Dotfiles Manager 4 | # Copyright (C) 2015-2021 Tim Byrne and Martin Zuther 5 | 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | 20 | YADM_CHECKSUMS="$YADM_HOOK_DIR/files.checksums" 21 | WARNING_MESSAGE="Checksums were not verified" 22 | 23 | # unpack exported array; filenames including a newline character (\n) 24 | # are NOT supported 25 | OLD_IFS="$IFS" 26 | IFS=$'\n' 27 | YADM_ENCRYPT_INCLUDE_FILES=( $YADM_ENCRYPT_INCLUDE_FILES ) 28 | IFS="$OLD_IFS" 29 | 30 | 31 | function get_checksum_command { 32 | # check if "shasum" exists and supports the algorithm (which is 33 | # tested by sending an empty string to "shasum") 34 | if command -v "shasum" > /dev/null && printf "" | shasum --algorithm "256" &> /dev/null; then 35 | printf "shasum --algorithm 256" 36 | # check if "sha256sum" exists 37 | elif command -v "sha256sum" > /dev/null; then 38 | printf "sha256sum" 39 | # check if "gsha256sum" exists 40 | elif command -v "gsha256sum" > /dev/null; then 41 | printf "gsha256sum" 42 | else 43 | # display warning in bright yellow 44 | printf "\033[1;33m" >&2 45 | printf "\nWARNING: \"shasum\", \"sha256sum\" and \"gsha256sum\" not found. %s\n" "$WARNING_MESSAGE." >&2 46 | 47 | # reset output color 48 | printf "\033[0m" >&2 49 | 50 | # signal error 51 | return 1 52 | fi 53 | } 54 | 55 | 56 | # if there is no checksum file, exit with original status of yadm 57 | # command 58 | if [ ! -f "$YADM_CHECKSUMS" ]; then 59 | exit "$YADM_HOOK_EXIT" 60 | fi 61 | 62 | # get checksum command 63 | CHECKSUM_COMMAND=$(get_checksum_command) 64 | 65 | # no command found 66 | if (($?)); then 67 | # return original exit status of yadm command 68 | exit "$YADM_HOOK_EXIT" 69 | fi 70 | 71 | # check encrypted files for differences and capture output and error 72 | # messages 73 | YADM_CHECKSUM_OUTPUT=$($CHECKSUM_COMMAND --check "$YADM_CHECKSUMS" 2>&1) 74 | ERROR_CODE=$? 75 | 76 | # handle mismatched checksums and errors 77 | if (($ERROR_CODE)); then 78 | printf "\nSome SHA-256 sums do not match (or an error occurred):\n\n" 79 | 80 | # display differing files and errors (highlighted in red) 81 | printf "\033[0;31m" 82 | 83 | while IFS= read -r line; do 84 | # beautify output and get rid of unnecessary lines 85 | line="${line%%*: [Oo][Kk]}" 86 | line="${line%%: [Ff][Aa][Ii][Ll][Ee][Dd]}" 87 | line="${line##*WARNING:*did NOT match}" 88 | 89 | if [ -n "$line" ]; then 90 | printf "%s\n" "$line" 91 | fi 92 | done <<< "$YADM_CHECKSUM_OUTPUT" 93 | 94 | # reset output color 95 | printf "\033[0m" 96 | 97 | # display advice for differing files and signal error 98 | printf "\nConsider running either \"yadm encrypt\" or \"yadm decrypt\".\n" 99 | exit $ERROR_CODE 100 | fi 101 | -------------------------------------------------------------------------------- /contrib/hooks/parsing_full_command_example/README.md: -------------------------------------------------------------------------------- 1 | ## Example of parsing `$YADM_HOOK_FULL_COMMAND` 2 | 3 | Contributed by Tim Byrne 4 | 5 | Hook | Description 6 | ---- | ----------- 7 | pre_log | Provides an example of parsing `$YADM_HOOK_FULL_COMMAND` in Bash 8 | -------------------------------------------------------------------------------- /contrib/hooks/parsing_full_command_example/pre_log: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # yadm exposes all parameters of the command which triggers a hook. Those 4 | # parameters are exported as the environment variable YADM_HOOK_FULL_COMMAND. 5 | # Any spaces, tabs, or backslashes in those parameters are escaped with a 6 | # backslash. The function `parse_full_command()` is a demonstration of parsing 7 | # those values which may be escaped. 8 | 9 | function parse_full_command() { 10 | local delim=$'\x1e' # ASCII Record Separator 11 | local space=$'\x1f' # ASCII Unit Separator 12 | local tab=$'\t' # ASCII TAB 13 | local cmd 14 | cmd="$YADM_HOOK_FULL_COMMAND" 15 | cmd="${cmd//\\ /$space}" # swap escaped spaces for `1f` 16 | cmd="${cmd//\\\\/\\}" # fix escaped backslashes 17 | cmd="${cmd//\\$tab/$tab}" # fix escaped tabs 18 | cmd="${cmd// /$delim}" # convert space delimiters to `1c` 19 | cmd="${cmd//$space/ }" # convert `1f` back to spaces 20 | # parse data into an array 21 | IFS=$delim read -r -a full_cmd <<< "$cmd" 22 | } 23 | parse_full_command 24 | for param in "${full_cmd[@]}"; do 25 | echo "Parameter: '$param'" 26 | done 27 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pytest.ini_options] 2 | cache_dir = "/tmp" 3 | addopts = "-ra" 4 | markers = [ 5 | "deprecated", # marks tests for deprecated features (deselect with '-m "not deprecated"') 6 | ] 7 | 8 | [tool.pylint.design] 9 | max-args = 14 10 | max-positional-arguments = 10 11 | max-locals = 28 12 | max-attributes = 8 13 | max-statements = 65 14 | 15 | [tool.pylint.format] 16 | max-line-length = 120 17 | 18 | [tool.pylint."messages control"] 19 | disable = [ 20 | "redefined-outer-name", 21 | ] 22 | 23 | [tool.pylint.similarities] 24 | ignore-imports = "yes" 25 | min-similarity-lines = 8 26 | 27 | [tool.black] 28 | line-length = 120 29 | 30 | [tool.isort] 31 | line_length = 120 32 | profile = "black" 33 | -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.10 2 | 3 | # Shellcheck and esh versions 4 | ARG SC_VER=0.10.0 5 | ARG ESH_VER=0.3.2 6 | 7 | # Install prerequisites and configure UTF-8 locale 8 | RUN \ 9 | echo "en_US.UTF-8 UTF-8" > /etc/locale.gen \ 10 | && apt-get update \ 11 | && DEBIAN_FRONTEND=noninteractive \ 12 | apt-get install -y --no-install-recommends \ 13 | expect \ 14 | git \ 15 | gnupg \ 16 | j2cli \ 17 | locales \ 18 | lsb-release \ 19 | make \ 20 | man \ 21 | python3-pip \ 22 | vim-tiny \ 23 | xz-utils \ 24 | && rm -rf /var/lib/apt/lists/* \ 25 | && update-locale LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8' 26 | 27 | ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8' 28 | 29 | # Convenience settings for the testbed's root account 30 | RUN echo 'set -o vi' >> /root/.bashrc 31 | 32 | # Create a flag to identify when running inside the yadm testbed 33 | RUN touch /.yadmtestbed 34 | 35 | # Install shellcheck 36 | ADD https://github.com/koalaman/shellcheck/releases/download/v$SC_VER/shellcheck-v$SC_VER.linux.x86_64.tar.xz /opt 37 | RUN cd /opt \ 38 | && tar xf shellcheck-v$SC_VER.linux.x86_64.tar.xz \ 39 | && rm -f shellcheck-v$SC_VER.linux.x86_64.tar.xz \ 40 | && ln -s /opt/shellcheck-v$SC_VER/shellcheck /usr/local/bin 41 | 42 | # Install requirements 43 | COPY test/requirements.txt /tmp/requirements.txt 44 | RUN python3 -m pip install --break-system-packages -r /tmp/requirements.txt \ 45 | && rm -f /tmp/requirements 46 | 47 | # Install esh 48 | ADD https://raw.githubusercontent.com/jirutka/esh/v$ESH_VER/esh /usr/local/bin 49 | RUN chmod +x /usr/local/bin/esh 50 | 51 | # Create workdir and dummy Makefile to be used if no /yadm volume is mounted 52 | RUN mkdir /yadm \ 53 | && echo "test:" > /yadm/Makefile \ 54 | && echo "\t@echo 'The yadm project must be mounted at /yadm'" >> /yadm/Makefile \ 55 | && echo "\t@echo 'Try using a docker parameter like -v \"\$\$PWD:/yadm:ro\"'" >> /yadm/Makefile \ 56 | && echo "\t@false" >> /yadm/Makefile 57 | 58 | # Include released versions of yadm to test upgrades 59 | ADD https://raw.githubusercontent.com/yadm-dev/yadm/1.12.0/yadm /usr/local/bin/yadm-1.12.0 60 | ADD https://raw.githubusercontent.com/yadm-dev/yadm/2.5.0/yadm /usr/local/bin/yadm-2.5.0 61 | RUN chmod +x /usr/local/bin/yadm-* 62 | 63 | # Configure git to make it easier to test yadm manually 64 | RUN git config --system user.email "test@yadm.io" \ 65 | && git config --system user.name "Yadm Test" 66 | 67 | # /yadm will be the work directory for all tests 68 | # docker commands should mount the local yadm project as /yadm 69 | WORKDIR /yadm 70 | 71 | # By default, run all tests defined 72 | CMD make test 73 | -------------------------------------------------------------------------------- /test/ownertrust.txt: -------------------------------------------------------------------------------- 1 | F8BBFC746C58945442349BCEBA54FFD04C599B1A:6: 2 | -------------------------------------------------------------------------------- /test/pinentry-mock: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This program is a custom mock pinentry program 3 | # It uses whatever password is found in the /tmp directory 4 | # If the password is empty, replies CANCEL causing an error to similate invalid 5 | # credentials 6 | echo "OK Pleased to meet you" 7 | while read -r line; do 8 | if [[ $line =~ GETPIN ]]; then 9 | password="$(cat "$GNUPGHOME/mock-password" 2>/dev/null)" 10 | if [ -n "$password" ]; then 11 | echo "D $password" 12 | echo "OK"; 13 | else 14 | echo "CANCEL"; 15 | fi 16 | else 17 | echo "OK"; 18 | fi 19 | done 20 | -------------------------------------------------------------------------------- /test/requirements.txt: -------------------------------------------------------------------------------- 1 | black==24.10.0 2 | envtpl 3 | flake8==7.1.1 4 | isort==5.13.2 5 | j2cli 6 | pylint==3.3.1 7 | pytest==8.3.3 8 | yamllint==1.35.1 9 | -------------------------------------------------------------------------------- /test/test_alt_copy.py: -------------------------------------------------------------------------------- 1 | """Test yadm.alt-copy""" 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "setting, expect_link, pre_existing", 10 | [ 11 | (None, True, None), 12 | (True, False, None), 13 | (False, True, None), 14 | (True, False, "link"), 15 | (True, False, "file"), 16 | ], 17 | ids=[ 18 | "unset", 19 | "true", 20 | "false", 21 | "pre-existing symlink", 22 | "pre-existing file", 23 | ], 24 | ) 25 | @pytest.mark.usefixtures("ds1_copy") 26 | def test_alt_copy(runner, yadm_cmd, paths, tst_sys, setting, expect_link, pre_existing): 27 | """Test yadm.alt-copy""" 28 | 29 | if setting is not None: 30 | os.system(" ".join(yadm_cmd("config", "yadm.alt-copy", str(setting)))) 31 | 32 | expected_content = f"test_alt_copy##os.{tst_sys}" 33 | 34 | alt_path = paths.work.join("test_alt_copy") 35 | if pre_existing == "symlink": 36 | alt_path.mklinkto(expected_content) 37 | elif pre_existing == "file": 38 | alt_path.write("wrong content") 39 | 40 | run = runner(yadm_cmd("alt")) 41 | assert run.success 42 | assert run.err == "" 43 | action = "Copying" if setting is True else "Linking" 44 | assert action in run.out 45 | 46 | assert alt_path.read() == expected_content 47 | assert alt_path.islink() == expect_link 48 | -------------------------------------------------------------------------------- /test/test_assert_private_dirs.py: -------------------------------------------------------------------------------- 1 | """Test asserting private directories""" 2 | 3 | import os 4 | import re 5 | 6 | import pytest 7 | 8 | pytestmark = pytest.mark.usefixtures("ds1_copy") 9 | PRIVATE_DIRS = [".gnupg", ".ssh"] 10 | 11 | 12 | @pytest.mark.parametrize("home", [True, False], ids=["home", "not-home"]) 13 | def test_pdirs_missing(runner, yadm_cmd, paths, home): 14 | """Private dirs (private dirs missing) 15 | 16 | When a git command is run 17 | And private directories are missing 18 | Create private directories prior to command 19 | """ 20 | 21 | # confirm directories are missing at start 22 | for pdir in PRIVATE_DIRS: 23 | path = paths.work.join(pdir) 24 | if path.exists(): 25 | path.remove() 26 | assert not path.exists() 27 | 28 | env = {"DEBUG": "yes"} 29 | if home: 30 | env["HOME"] = paths.work 31 | 32 | # run status 33 | run = runner(command=yadm_cmd("status"), env=env) 34 | assert run.success 35 | assert run.err == "" 36 | assert "On branch master" in run.out 37 | 38 | # confirm directories are created 39 | # and are protected 40 | for pdir in PRIVATE_DIRS: 41 | path = paths.work.join(pdir) 42 | if home: 43 | assert path.exists() 44 | assert oct(path.stat().mode).endswith("00"), "Directory is " "not secured" 45 | else: 46 | assert not path.exists() 47 | 48 | # confirm directories are created before command is run: 49 | if home: 50 | assert re.search( 51 | r"Creating.+\.(gnupg|ssh).+Creating.+\.(gnupg|ssh).+Running git command git status", run.out, re.DOTALL 52 | ), "directories created before command is run" 53 | 54 | 55 | def test_pdirs_missing_apd_false(runner, yadm_cmd, paths): 56 | """Private dirs (private dirs missing / yadm.auto-private-dirs=false) 57 | 58 | When a git command is run 59 | And private directories are missing 60 | But auto-private-dirs is false 61 | Do not create private dirs 62 | """ 63 | 64 | # confirm directories are missing at start 65 | for pdir in PRIVATE_DIRS: 66 | path = paths.work.join(pdir) 67 | if path.exists(): 68 | path.remove() 69 | assert not path.exists() 70 | 71 | # set configuration 72 | os.system(" ".join(yadm_cmd("config", "--bool", "yadm.auto-private-dirs", "false"))) 73 | 74 | # run status 75 | run = runner(command=yadm_cmd("status")) 76 | assert run.success 77 | assert run.err == "" 78 | assert "On branch master" in run.out 79 | 80 | # confirm directories are STILL missing 81 | for pdir in PRIVATE_DIRS: 82 | assert not paths.work.join(pdir).exists() 83 | 84 | 85 | def test_pdirs_exist_apd_false(runner, yadm_cmd, paths): 86 | """Private dirs (private dirs exist / yadm.auto-perms=false) 87 | 88 | When a git command is run 89 | And private directories exist 90 | And yadm is configured not to auto update perms 91 | Do not alter directories 92 | """ 93 | 94 | # create permissive directories 95 | for pdir in PRIVATE_DIRS: 96 | path = paths.work.join(pdir) 97 | if not path.isdir(): 98 | path.mkdir() 99 | path.chmod(0o777) 100 | assert oct(path.stat().mode).endswith("77"), "Directory is secure." 101 | 102 | # set configuration 103 | os.system(" ".join(yadm_cmd("config", "--bool", "yadm.auto-perms", "false"))) 104 | 105 | # run status 106 | run = runner(command=yadm_cmd("status")) 107 | assert run.success 108 | assert run.err == "" 109 | assert "On branch master" in run.out 110 | 111 | # created directories are STILL permissive 112 | for pdir in PRIVATE_DIRS: 113 | path = paths.work.join(pdir) 114 | assert oct(path.stat().mode).endswith("77"), "Directory is secure" 115 | -------------------------------------------------------------------------------- /test/test_bootstrap.py: -------------------------------------------------------------------------------- 1 | """Test bootstrap""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "exists, executable, code, expect", 8 | [ 9 | (False, False, 1, "Cannot execute bootstrap"), 10 | (True, False, 1, "is not an executable program"), 11 | (True, True, 123, "Bootstrap successful"), 12 | ], 13 | ids=[ 14 | "missing", 15 | "not executable", 16 | "executable", 17 | ], 18 | ) 19 | def test_bootstrap(runner, yadm_cmd, paths, exists, executable, code, expect): 20 | """Test bootstrap command""" 21 | if exists: 22 | paths.bootstrap.write("") 23 | if executable: 24 | paths.bootstrap.write("#!/bin/bash\n" f"echo {expect}\n" f"exit {code}\n") 25 | paths.bootstrap.chmod(0o775) 26 | run = runner(command=yadm_cmd("bootstrap")) 27 | assert run.code == code 28 | if exists and executable: 29 | assert run.err == "" 30 | assert expect in run.out 31 | else: 32 | assert expect in run.err 33 | assert run.out == "" 34 | -------------------------------------------------------------------------------- /test/test_clean.py: -------------------------------------------------------------------------------- 1 | """Test clean""" 2 | 3 | 4 | def test_clean_command(runner, yadm_cmd): 5 | """Run with clean command""" 6 | run = runner(command=yadm_cmd("clean")) 7 | # do nothing, this is a dangerous Git command when managing dot files 8 | # report the command as disabled and exit as a failure 9 | assert run.failure 10 | assert run.out == "" 11 | assert "disabled" in run.err 12 | -------------------------------------------------------------------------------- /test/test_config.py: -------------------------------------------------------------------------------- 1 | """Test config""" 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | TEST_SECTION = "test" 8 | TEST_ATTRIBUTE = "attribute" 9 | TEST_KEY = f"{TEST_SECTION}.{TEST_ATTRIBUTE}" 10 | TEST_VALUE = "testvalue" 11 | TEST_FILE = f"[{TEST_SECTION}]\n\t{TEST_ATTRIBUTE} = {TEST_VALUE}" 12 | 13 | 14 | def test_config_no_params(runner, yadm_cmd, supported_configs): 15 | """No parameters 16 | 17 | Display instructions 18 | Display supported configs 19 | Exit with 0 20 | """ 21 | 22 | run = runner(yadm_cmd("config")) 23 | 24 | assert run.success 25 | assert run.err == "" 26 | assert "Please read the CONFIGURATION section" in run.out 27 | for config in supported_configs: 28 | assert config in run.out 29 | 30 | 31 | def test_config_read_missing(runner, yadm_cmd): 32 | """Read missing attribute 33 | 34 | Display an empty value 35 | Exit with 0 36 | """ 37 | 38 | run = runner(yadm_cmd("config", TEST_KEY)) 39 | 40 | assert run.success 41 | assert run.err == "" 42 | assert run.out == "" 43 | 44 | 45 | def test_config_write(runner, yadm_cmd, paths): 46 | """Write attribute 47 | 48 | Display no output 49 | Update configuration file 50 | Exit with 0 51 | """ 52 | 53 | run = runner(yadm_cmd("config", TEST_KEY, TEST_VALUE)) 54 | 55 | assert run.success 56 | assert run.err == "" 57 | assert run.out == "" 58 | assert paths.config.read().strip() == TEST_FILE 59 | 60 | 61 | def test_config_read(runner, yadm_cmd, paths): 62 | """Read attribute 63 | 64 | Display value 65 | Exit with 0 66 | """ 67 | 68 | paths.config.write(TEST_FILE) 69 | run = runner(yadm_cmd("config", TEST_KEY)) 70 | 71 | assert run.success 72 | assert run.err == "" 73 | assert run.out.strip() == TEST_VALUE 74 | 75 | 76 | def test_config_update(runner, yadm_cmd, paths): 77 | """Update attribute 78 | 79 | Display no output 80 | Update configuration file 81 | Exit with 0 82 | """ 83 | 84 | paths.config.write(TEST_FILE) 85 | 86 | run = runner(yadm_cmd("config", TEST_KEY, TEST_VALUE + "extra")) 87 | 88 | assert run.success 89 | assert run.err == "" 90 | assert run.out == "" 91 | 92 | assert paths.config.read().strip() == TEST_FILE + "extra" 93 | 94 | 95 | @pytest.mark.usefixtures("ds1_repo_copy") 96 | def test_config_local_read(runner, yadm_cmd, paths, supported_local_configs): 97 | """Read local attribute 98 | 99 | Display value from the repo config 100 | Exit with 0 101 | """ 102 | 103 | # populate test values 104 | for config in supported_local_configs: 105 | os.system(f'GIT_DIR="{paths.repo}" ' f'git config --local "{config}" "value_of_{config}"') 106 | 107 | # run yadm config 108 | for config in supported_local_configs: 109 | run = runner(yadm_cmd("config", config)) 110 | assert run.success 111 | assert run.err == "" 112 | assert run.out.strip() == f"value_of_{config}" 113 | 114 | 115 | @pytest.mark.usefixtures("ds1_repo_copy") 116 | def test_config_local_write(runner, yadm_cmd, paths, supported_local_configs): 117 | """Write local attribute 118 | 119 | Display no output 120 | Write value to the repo config 121 | Exit with 0 122 | """ 123 | 124 | # run yadm config 125 | for config in supported_local_configs: 126 | run = runner(yadm_cmd("config", config, f"value_of_{config}")) 127 | assert run.success 128 | assert run.err == "" 129 | assert run.out == "" 130 | 131 | # verify test values 132 | for config in supported_local_configs: 133 | run = runner(command=("git", "config", config), env={"GIT_DIR": paths.repo}) 134 | assert run.success 135 | assert run.err == "" 136 | assert run.out.strip() == f"value_of_{config}" 137 | 138 | 139 | def test_config_without_parent_directory(runner, yadm_cmd, paths): 140 | """Write/read attribute to/from config file with non-existent parent dir 141 | 142 | Update configuration file 143 | Display value 144 | Exit with 0 145 | """ 146 | 147 | config_file = paths.root + "/folder/does/not/exist/config" 148 | 149 | run = runner(yadm_cmd("--yadm-config", config_file, "config", TEST_KEY, TEST_VALUE)) 150 | 151 | assert run.success 152 | assert run.err == "" 153 | assert run.out == "" 154 | 155 | run = runner(yadm_cmd("--yadm-config", config_file, "config", TEST_KEY)) 156 | 157 | assert run.success 158 | assert run.err == "" 159 | assert run.out.strip() == TEST_VALUE 160 | -------------------------------------------------------------------------------- /test/test_enter.py: -------------------------------------------------------------------------------- 1 | """Test enter""" 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "shell, success", 10 | [ 11 | ("delete", True), # if there is no shell variable, bash creates it 12 | ("", False), 13 | ("/usr/bin/env", True), 14 | ("noexec", False), 15 | ], 16 | ids=[ 17 | "shell-missing", 18 | "shell-empty", 19 | "shell-env", 20 | "shell-noexec", 21 | ], 22 | ) 23 | @pytest.mark.usefixtures("ds1_copy") 24 | def test_enter(runner, yadm_cmd, paths, shell, success): 25 | """Enter tests""" 26 | env = os.environ.copy() 27 | if shell == "delete": 28 | # remove shell 29 | if "SHELL" in env: 30 | del env["SHELL"] 31 | elif shell == "noexec": 32 | # specify a non-executable path 33 | noexec = paths.root.join("noexec") 34 | noexec.write("") 35 | noexec.chmod(0o664) 36 | env["SHELL"] = str(noexec) 37 | else: 38 | env["SHELL"] = shell 39 | 40 | run = runner(command=yadm_cmd("enter"), env=env) 41 | assert run.success == success 42 | prompt = f"yadm shell ({paths.repo})" 43 | if success: 44 | assert run.out.startswith("Entering yadm repo") 45 | assert run.out.rstrip().endswith("Leaving yadm repo") 46 | assert run.err == "" 47 | else: 48 | assert "does not refer to an executable" in run.err 49 | if "env" in shell: 50 | assert f"GIT_DIR={paths.repo}" in run.out 51 | assert f"GIT_WORK_TREE={paths.work}" in run.out 52 | assert f"PROMPT={prompt}" in run.out 53 | assert f"PS1={prompt}" in run.out 54 | 55 | 56 | @pytest.mark.parametrize( 57 | "shell, opts, path", 58 | [ 59 | ("bash", "--norc", "\\w"), 60 | ("csh", "-f", "%~"), 61 | ("zsh", "-f", "%~"), 62 | ], 63 | ids=[ 64 | "bash", 65 | "csh", 66 | "zsh", 67 | ], 68 | ) 69 | @pytest.mark.parametrize( 70 | "cmd", 71 | [False, "cmd", "cmd-bad-exit"], 72 | ids=["no-cmd", "cmd", "cmd-bad-exit"], 73 | ) 74 | @pytest.mark.parametrize( 75 | "term", 76 | ["", "dumb"], 77 | ids=["term-empty", "term-dumb"], 78 | ) 79 | @pytest.mark.usefixtures("ds1_copy") 80 | def test_enter_shell_ops(runner, yadm_cmd, paths, shell, opts, path, cmd, term): 81 | """Enter tests for specific shell options""" 82 | 83 | change_exit = "\nfalse" if cmd == "cmd-bad-exit" else "" 84 | 85 | # Create custom shell to detect options passed 86 | custom_shell = paths.root.join(shell) 87 | custom_shell.write(f"#!/bin/sh\necho OPTS=$*\necho PROMPT=$PROMPT{change_exit}") 88 | custom_shell.chmod(0o775) 89 | 90 | test_cmd = ["test1", "test2", "test3"] 91 | 92 | enter_cmd = ["enter"] 93 | if cmd: 94 | enter_cmd += test_cmd 95 | 96 | env = os.environ.copy() 97 | env["TERM"] = term 98 | env["SHELL"] = custom_shell 99 | 100 | if shell == "zsh" and term == "dumb": 101 | opts += " --no-zle" 102 | 103 | run = runner(command=yadm_cmd(*enter_cmd), env=env) 104 | if cmd == "cmd-bad-exit": 105 | assert run.failure 106 | else: 107 | assert run.success 108 | assert run.err == "" 109 | assert f"OPTS={opts}" in run.out 110 | assert f"PROMPT=yadm shell ({paths.repo}) {path} >" in run.out 111 | if cmd: 112 | assert "-c " + " ".join(test_cmd) in run.out 113 | assert "Entering yadm repo" not in run.out 114 | assert "Leaving yadm repo" not in run.out 115 | else: 116 | assert "Entering yadm repo" in run.out 117 | assert "Leaving yadm repo" in run.out 118 | -------------------------------------------------------------------------------- /test/test_ext_crypt.py: -------------------------------------------------------------------------------- 1 | """Test external encryption commands""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "crypt", 8 | [False, "installed", "installed-but-failed"], 9 | ids=["not-installed", "installed", "installed-but-failed"], 10 | ) 11 | @pytest.mark.parametrize( 12 | "cmd,var", 13 | [ 14 | ["git_crypt", "GIT_CRYPT_PROGRAM"], 15 | ["transcrypt", "TRANSCRYPT_PROGRAM"], 16 | ], 17 | ids=["git-crypt", "transcrypt"], 18 | ) 19 | def test_ext_encryption(runner, yadm, paths, tmpdir, crypt, cmd, var): 20 | """External encryption tests""" 21 | 22 | paths.repo.ensure(dir=True) 23 | bindir = tmpdir.mkdir("bin") 24 | pgm = bindir.join("test-ext-crypt") 25 | 26 | if crypt: 27 | pgm.write("#!/bin/sh\necho ext-crypt ran\n") 28 | pgm.chmod(0o775) 29 | if crypt == "installed-but-failed": 30 | pgm.write("false\n", mode="a") 31 | 32 | script = f""" 33 | YADM_TEST=1 source {yadm} 34 | YADM_REPO={paths.repo} 35 | {var}="{pgm}" 36 | {cmd} "param1" 37 | """ 38 | 39 | run = runner(command=["bash"], inp=script) 40 | 41 | if crypt: 42 | if crypt == "installed-but-failed": 43 | assert run.failure 44 | else: 45 | assert run.success 46 | assert run.out.strip() == "ext-crypt ran" 47 | assert run.err == "" 48 | else: 49 | assert run.failure 50 | assert f"command '{pgm}' cannot be located" in run.err 51 | -------------------------------------------------------------------------------- /test/test_git.py: -------------------------------------------------------------------------------- 1 | """Test git""" 2 | 3 | import re 4 | 5 | import pytest 6 | 7 | 8 | @pytest.mark.usefixtures("ds1_copy") 9 | def test_git(runner, yadm_cmd, paths): 10 | """Test series of passthrough git commands 11 | 12 | Passthru unknown commands to Git 13 | Git command 'add' - badfile 14 | Git command 'add' 15 | Git command 'status' 16 | Git command 'commit' 17 | Git command 'log' 18 | """ 19 | 20 | # passthru unknown commands to Git 21 | run = runner(command=yadm_cmd("bogus")) 22 | assert run.failure 23 | assert "git: 'bogus' is not a git command." in run.err 24 | assert "See 'git --help'" in run.err 25 | assert run.out == "" 26 | 27 | # git command 'add' - badfile 28 | run = runner(command=yadm_cmd("add", "-v", "does_not_exist")) 29 | assert run.code == 128 30 | assert "pathspec 'does_not_exist' did not match any files" in run.err 31 | assert run.out == "" 32 | 33 | # git command 'add' 34 | newfile = paths.work.join("test_git") 35 | newfile.write("test_git") 36 | run = runner(command=yadm_cmd("add", "-v", str(newfile))) 37 | assert run.success 38 | assert run.err == "" 39 | assert "add 'test_git'" in run.out 40 | 41 | # git command 'status' 42 | run = runner(command=yadm_cmd("status")) 43 | assert run.success 44 | assert run.err == "" 45 | assert re.search(r"new file:\s+test_git", run.out) 46 | 47 | # git command 'commit' 48 | run = runner(command=yadm_cmd("commit", "-m", "Add test_git")) 49 | assert run.success 50 | assert run.err == "" 51 | assert "1 file changed" in run.out 52 | assert "1 insertion" in run.out 53 | assert re.search(r"create mode .+ test_git", run.out) 54 | 55 | # git command 'log' 56 | run = runner(command=yadm_cmd("log", "--oneline")) 57 | assert run.success 58 | assert run.err == "" 59 | assert "Add test_git" in run.out 60 | -------------------------------------------------------------------------------- /test/test_help.py: -------------------------------------------------------------------------------- 1 | """Test help""" 2 | 3 | import pytest 4 | 5 | 6 | def test_missing_command(runner, yadm_cmd): 7 | """Run without any command""" 8 | run = runner(command=yadm_cmd()) 9 | assert run.failure 10 | assert run.err == "" 11 | assert run.out.startswith("Usage: yadm") 12 | 13 | 14 | @pytest.mark.parametrize("cmd", ["--help", "help"]) 15 | def test_help_command(runner, yadm_cmd, cmd): 16 | """Run with help command""" 17 | run = runner(command=yadm_cmd(cmd)) 18 | assert run.failure 19 | assert run.err == "" 20 | assert run.out.startswith("Usage: yadm") 21 | -------------------------------------------------------------------------------- /test/test_hooks.py: -------------------------------------------------------------------------------- 1 | """Test hooks""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "pre, pre_code, post, post_code", 8 | [ 9 | (False, 0, False, 0), 10 | (True, 0, False, 0), 11 | (True, 5, False, 0), 12 | (False, 0, True, 0), 13 | (False, 0, True, 5), 14 | (True, 0, True, 0), 15 | (True, 5, True, 5), 16 | ], 17 | ids=[ 18 | "no-hooks", 19 | "pre-success", 20 | "pre-fail", 21 | "post-success", 22 | "post-fail", 23 | "pre-post-success", 24 | "pre-post-fail", 25 | ], 26 | ) 27 | @pytest.mark.parametrize("cmd", ["--version", "version"]) 28 | def test_hooks(runner, yadm_cmd, paths, cmd, pre, pre_code, post, post_code): 29 | """Test pre/post hook""" 30 | 31 | # generate hooks 32 | if pre: 33 | create_hook(paths, "pre_version", pre_code) 34 | if post: 35 | create_hook(paths, "post_version", post_code) 36 | 37 | # run yadm 38 | run = runner(yadm_cmd(cmd)) 39 | # when a pre hook fails, yadm should exit with the hook's code 40 | assert run.code == pre_code 41 | assert run.err == "" 42 | 43 | if pre: 44 | assert "HOOK:pre_version" in run.out 45 | # if pre hook is missing or successful, yadm itself should exit 0 46 | if run.success: 47 | if post: 48 | assert "HOOK:post_version" in run.out 49 | else: 50 | # when a pre hook fails, yadm should not run the command 51 | assert "version will not be run" in run.out 52 | # when a pre hook fails, yadm should not run the post hook 53 | assert "HOOK:post_version" not in run.out 54 | 55 | 56 | # repo fixture is needed to test the population of YADM_HOOK_WORK 57 | @pytest.mark.usefixtures("ds1_repo_copy") 58 | def test_hook_env(runner, yadm_cmd, paths): 59 | """Test hook environment""" 60 | 61 | # test will be done with a non existent "git" passthru command 62 | # which should exit with a failing code 63 | cmd = "passthrucmd" 64 | 65 | # write the hook 66 | hook = paths.hooks.join(f"post_{cmd}") 67 | hook.write("#!/bin/bash\nenv\ndeclare\n") 68 | hook.chmod(0o755) 69 | 70 | run = runner(yadm_cmd(cmd, "extra_args")) 71 | 72 | # expect passthru to fail 73 | assert run.failure 74 | assert f"'{cmd}' is not a git command" in run.err 75 | 76 | # verify hook environment 77 | assert "YADM_HOOK_EXIT=1\n" in run.out 78 | assert f"YADM_HOOK_COMMAND={cmd}\n" in run.out 79 | assert f"YADM_HOOK_DIR={paths.yadm}\n" in run.out 80 | assert f"YADM_HOOK_FULL_COMMAND={cmd} extra_args\n" in run.out 81 | assert f"YADM_HOOK_REPO={paths.repo}\n" in run.out 82 | assert f"YADM_HOOK_WORK={paths.work}\n" in run.out 83 | assert "YADM_ENCRYPT_INCLUDE_FILES=\n" in run.out 84 | 85 | # verify the hook environment contains certain exported functions 86 | for func in [ 87 | "builtin_dirname", 88 | "relative_path", 89 | "unix_path", 90 | "mixed_path", 91 | ]: 92 | assert f"BASH_FUNC_{func}" in run.out 93 | 94 | # verify the hook environment contains the list of encrypted files 95 | script = f""" 96 | YADM_TEST=1 source {paths.pgm} 97 | YADM_HOOKS="{paths.hooks}" 98 | HOOK_COMMAND="{cmd}" 99 | ENCRYPT_INCLUDE_FILES=(a b c) 100 | invoke_hook "post" 101 | """ 102 | run = runner(command=["bash"], inp=script) 103 | assert run.success 104 | assert run.err == "" 105 | assert "YADM_ENCRYPT_INCLUDE_FILES=a\nb\nc\n" in run.out 106 | 107 | 108 | def test_escaped(runner, yadm_cmd, paths): 109 | """Test escaped values in YADM_HOOK_FULL_COMMAND""" 110 | 111 | # test will be done with a non existent "git" passthru command 112 | # which should exit with a failing code 113 | cmd = "passthrucmd" 114 | 115 | # write the hook 116 | hook = paths.hooks.join(f"post_{cmd}") 117 | hook.write("#!/bin/bash\nenv\n") 118 | hook.chmod(0o755) 119 | 120 | run = runner(yadm_cmd(cmd, "a b", "c\td", "e\\f")) 121 | 122 | # expect passthru to fail 123 | assert run.failure 124 | 125 | # verify escaped values 126 | assert f"YADM_HOOK_FULL_COMMAND={cmd} a\\ b c\\\td e\\\\f\n" in run.out 127 | 128 | 129 | @pytest.mark.parametrize("condition", ["exec", "no-exec", "mingw"]) 130 | def test_executable(runner, paths, condition): 131 | """Verify hook must be exectuable""" 132 | cmd = "version" 133 | hook = paths.hooks.join(f"pre_{cmd}") 134 | hook.write("#!/bin/sh\necho HOOK\n") 135 | hook.chmod(0o644) 136 | if condition == "exec": 137 | hook.chmod(0o755) 138 | 139 | mingw = 'OPERATING_SYSTEM="MINGWx"' if condition == "mingw" else "" 140 | script = f""" 141 | YADM_TEST=1 source {paths.pgm} 142 | YADM_HOOKS="{paths.hooks}" 143 | HOOK_COMMAND="{cmd}" 144 | {mingw} 145 | invoke_hook "pre" 146 | """ 147 | run = runner(command=["bash"], inp=script) 148 | 149 | if condition != "mingw": 150 | assert run.success 151 | assert run.err == "" 152 | else: 153 | assert run.failure 154 | assert "Permission denied" in run.err 155 | 156 | if condition == "exec": 157 | assert "HOOK" in run.out 158 | elif condition == "no-exec": 159 | assert "HOOK" not in run.out 160 | 161 | 162 | def create_hook(paths, name, code): 163 | """Create hook""" 164 | hook = paths.hooks.join(name) 165 | hook.write("#!/bin/sh\n" f"echo HOOK:{name}\n" f"exit {code}\n") 166 | hook.chmod(0o755) 167 | -------------------------------------------------------------------------------- /test/test_init.py: -------------------------------------------------------------------------------- 1 | """Test init""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "alt_work, repo_present, force", 8 | [ 9 | (False, False, False), 10 | (True, False, False), 11 | (False, True, False), 12 | (False, True, True), 13 | (True, True, True), 14 | ], 15 | ids=[ 16 | "simple", 17 | "-w", 18 | "existing repo", 19 | "-f", 20 | "-w & -f", 21 | ], 22 | ) 23 | @pytest.mark.usefixtures("ds1_work_copy") 24 | def test_init(runner, yadm_cmd, paths, repo_config, alt_work, repo_present, force): 25 | """Test init 26 | 27 | Repos should have attribs: 28 | - 0600 permissions 29 | - not bare 30 | - worktree = $HOME 31 | - showUntrackedFiles = no 32 | - yadm.managed = true 33 | """ 34 | 35 | # these tests will assume this for $HOME 36 | home = str(paths.root.mkdir("HOME")) 37 | 38 | # ds1_work_copy comes WITH an empty repo dir present. 39 | old_repo = paths.repo.join("old_repo") 40 | if repo_present: 41 | # Let's put some data in it, so we can confirm that data is gone when 42 | # forced to be overwritten. 43 | old_repo.write("old repo data") 44 | assert old_repo.isfile() 45 | else: 46 | paths.repo.remove() 47 | 48 | # command args 49 | args = ["init"] 50 | cwd = None 51 | if alt_work: 52 | if force: 53 | cwd = paths.work.dirname 54 | args.extend(["-w", paths.work.basename]) 55 | else: 56 | args.extend(["-w", paths.work]) 57 | if force: 58 | args.append("-f") 59 | 60 | # run init 61 | runner(["git", "config", "--global", "init.defaultBranch", "master"], env={"HOME": home}, cwd=cwd) 62 | run = runner(yadm_cmd(*args), env={"HOME": home}, cwd=cwd) 63 | 64 | if repo_present and not force: 65 | assert run.failure 66 | assert "repo already exists" in run.err 67 | assert old_repo.isfile(), "Missing original repo" 68 | else: 69 | assert run.success 70 | assert "Initialized empty shared Git repository" in run.out 71 | 72 | if repo_present: 73 | assert not old_repo.isfile(), "Original repo still exists" 74 | else: 75 | assert run.err == "" 76 | 77 | if alt_work: 78 | assert repo_config("core.worktree") == paths.work 79 | else: 80 | assert repo_config("core.worktree") == home 81 | 82 | # uniform repo assertions 83 | assert oct(paths.repo.stat().mode).endswith("00"), "Repo is not secure" 84 | assert repo_config("core.bare") == "false" 85 | assert repo_config("status.showUntrackedFiles") == "no" 86 | assert repo_config("yadm.managed") == "true" 87 | -------------------------------------------------------------------------------- /test/test_introspect.py: -------------------------------------------------------------------------------- 1 | """Test introspect""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "name", 8 | [ 9 | "", 10 | "invalid", 11 | "commands", 12 | "configs", 13 | "repo", 14 | "switches", 15 | ], 16 | ) 17 | def test_introspect_category(runner, yadm_cmd, paths, name, supported_commands, supported_configs, supported_switches): 18 | """Validate introspection category""" 19 | if name: 20 | run = runner(command=yadm_cmd("introspect", name)) 21 | else: 22 | run = runner(command=yadm_cmd("introspect")) 23 | 24 | assert run.success 25 | assert run.err == "" 26 | 27 | expected = [] 28 | if name == "commands": 29 | expected = supported_commands 30 | elif name == "configs": 31 | expected = supported_configs 32 | elif name == "switches": 33 | expected = supported_switches 34 | 35 | # assert values 36 | if name in ("", "invalid"): 37 | assert run.out == "" 38 | if name == "repo": 39 | assert run.out.rstrip() == paths.repo 40 | 41 | # make sure every expected value is present 42 | for value in expected: 43 | assert value in run.out 44 | # make sure nothing extra is present 45 | if expected: 46 | assert len(run.out.split()) == len(expected) 47 | -------------------------------------------------------------------------------- /test/test_key: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PRIVATE KEY BLOCK----- 2 | Version: GnuPG v1 3 | 4 | lQOYBFcWplIBCACyT3gCpP6QKuDGnSd1xsCydJhI1KnLPFR/YxuznkDfXVXMY6WC 5 | f29WiknfpqwARkNEt2j5o0AxoYKVtZSeLAR2dIwMRJMMfZerezMbMTizLA9Dc+U4 6 | NzEWoJwr+p1PnQcz5IdIT/O95UFswyBlkk6m7oWtZ8eYHDr8O+DYvj8B2fcm8rfq 7 | 7c5IcwuzTgPMfz+VJynuB4WarS71Qh84t7eWhCbAZAiC8OEdSqHRli/0T02o04Mx 8 | jVRdxwImJfOc81B4oZr60tdsadwfvcW5dXdNL/kavCH25+QAfEobRU+/y1JI0yx+ 9 | tGYlQ1hkVQYDUt7eA5/9sK9AMTYM0plnJk73ABEBAAEAB/9GeBKxVNzIRDHePKim 10 | KrzoKh0vF2DdUcQBLj158K6pt/zbEHyOROfPF0sXyQqL9zjJlQS3OBX8J1zw5rjM 11 | BBBlci0RAh7tXktNOZzaf8rtQJntqgVqgKF1VFc0KFD4cFIy53uxj+t/3nVLUxhg 12 | HADah0SsYennSyzil5WGgzVqeL1zct+fFf+MSPSIiQJqZbD2QbyLk8IRNcnRyes+ 13 | 78brrZkPYNiNv6k/aZejKCAwjSqU6kMNHr1rwxvaY3g5oL4662bOZXBTsp4qvaJK 14 | jb7LtB72Mtj++T+qBJzDdhty/OQGrsJjMDi6IdIllW7cc+s0FFCH3b+biB4BoKW7 15 | bnvpBADOb8gALC8v1WD7cEFZ12gIk3IrRcDJD8taozS7jWna83rga9W7qz+eW2Gb 16 | vOVS+rNG5n/O0Bm1Uvr+y0+i7l21+8iECA3KlP09k+7XDGZUu+IzO4S8guzAu33k 17 | hlQFj5KwRaXx4nNEGUMZfX75NVHvpcN5W1eKTg1t27I+K1R2mQQA3R73F9FZmnVg 18 | 4VKvfPTgiwQcns8tOXnv/23BNpHqu14qG2E0Dh9xa5FTvtq6hrsKVdH61AU8dptX 19 | BnLTzG7xF0qEecFpYkmCuyqlVdVPrxBc+Q2PLxK66QpUX+/0m1R3pKGFJ/g+WLdz 20 | 8yMSwMX4W8pSH7QmxVhh4zojmYbTvA8EALE7JmahLUcU/GLs//0sd06XcdS42ENn 21 | cB2TpqtzLqR9im8tx1/rImWGJFzAvoaAsk4ATXwSoKBiUjmt0jRtVU0Etbm7QTRg 22 | ub247h4SNKcQyNBZ5eKIn93Cpt2vaTH7rKJ9y5UYAXmsgVrdW9lihaGOgHrgqkMO 23 | nZV5j17elMNfRl20J1lBRE0gVGVzdCAxIDx5YWRtLXRlc3QxQGxvY2VoaWxpb3Mu 24 | Y29tPokBOAQTAQIAIgUCVxamUgIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AA 25 | CgkQulT/0ExZmxprzQf9HxoC10h0/GKlzMoNqVhGcrknCD0LMYmx+A8n2qEKVqGG 26 | 9+Hsc5BNI/TQNKJUUsh3G/NGvIDhATKeKrGPI1ezIdpxubtynVJ5qPFOFe/tDFp3 27 | iMN00v0b64E8OLHXXM26D+fX5/5N6OI+UFaeUT8omrbXy67aAFy74Vm1Ybac2zni 28 | LuMtXLS65g23plAn509SXl/g1KPnXDIO8ccCn6/5o8s5ZSA3LKTQEtgwN2gX14rN 29 | n/9DvudpscelkWUWv6wxXOb9p9N/JmNOSGrQ2zyT1u6UWMBxkdgQ90+BZ+Y/wiCs 30 | lgBjC+dqU9ooJy7EtGD6PjJPunUBi3YjSteMOXnax50DmARXFqZSAQgA22z0PzyT 31 | 6hFfioVVax7zppRJDPQwW+l4+2N7eYUCNoSELhC/uKYwQIZfhRJlX4rkaVv8PgwK 32 | LdtPyZhHckxGNfsq6w2V/orVFc46dwCiYGsuqIXlu9+KVCsBB4/it8D56koBPPET 33 | kz5yZDqR7WtoKLbjTPjwOlJwPk/7o87d6CyAcWP6bzVTIiFM3XAXtvdDfXwL9Mj8 34 | wgTrDc6GFGiwz2VCMVNWASLPvPrGiqEjrt7zaLUrRaLwK81FJUtGcNu06KbZRP6G 35 | +Iu/9+UZ3hmIcZMJZtqNO87q7VHW6NecGRlrg/EZP6XyMTtk83w5aFrOvtzym0xc 36 | jkTOKGEE72UXVwARAQABAAf7BwcXT4suJZoG2FXq5XJpVVV8fXi4r8jrggmuo7a5 37 | 2msmHJ+WtGBGPVrQZl+vdX7qT+GNU6NpFAzpIkjJSQTeXs47kqmtuyhRKNChGLyh 38 | drsYFHetYvYG5Sk3cDmQhlgc6P8TyRLjkJy4ZzNlBxigjmVFJGr4rrWDOMuxAI8Y 39 | ll3/TFa+XrFeBUoFakiC1C8jIanaVCK21kQ2Qam3EKCfuASvxGiCLb/nZ84mDF2d 40 | GrLiUGA2GumP2cXS/ml8Q/YCjOmQMSTYkM9zFAUkLtfrIZY0/cqIotDOuAY7H3lJ 41 | u4NlJrenRUnYerjS2QOxm6DdXKu9ChtHJKOrlDMkl3z1SQQA3hQx/DI2BJeSnQLI 42 | CeO1yMvUf52Dg0e66t7yE0dUcgn4eaIRChMi8aWX3fv3CBVBqPrH5o1BLpqDSHt6 43 | fGg/za1sMljrtWslnE17UPPl9ZTnS5c1mcNkg3YoyHjGa9RAiEbEMwWF3mPyS+YT 44 | NuqL6F+KGmTRcTi3eTLEWOf5ltMEAPzxAldXAeconblzupQkuyjhlnlwJYYKzx7P 45 | nJK2rQW8eOJIPjNC/1xbvWw25Hh/ZNIFN/kWk+lol9PmIPVGp4yfWMOegCH3v6xz 46 | YZarAyhTqlRQQEeVBddyp2RV6r6+6pz5goTJLGyFiNCgTzMdhZn1U14lnE6ABJW8 47 | z62Jm/LtA/sFqOSV5PYOdaRRZ7kTBKRmQNQKyJhT5yjnYiI6ME6ds8n5f3lLDnte 48 | VMUt/IULRIRKQ3JExgciGaDYLhYIy0ZALrpeh5jshM9jPJGK6heaM90h8bnPAdxM 49 | waNbo+DtTGbHLqqMbVDMSPjO7wSrCuSzfRvTBgaC1puz2YjsN5C/CD9liQEfBBgB 50 | AgAJBQJXFqZSAhsMAAoJELpU/9BMWZsabE8IAI+z0v6Y+TPoJR7vHAu8twaEWV8E 51 | z2BAkLabe0IvZH3lvXtlJyhGKm9XIfKINKruwwM+ty+XRXzl3llPUEeylkkPZ4TV 52 | isKmCazO/M3+2AZ8lexNeJqzUitf5tStapkhoyZOfjbEtpddR9vqUoJQ6aWjYk/y 53 | YV9Uh5Za5YAb7QcaDIwxGHnCmxovwyUr2T7Z3b4k4O9lqwgjOCezZYYb6+BTnVmz 54 | +C2h9Pk+M1Fuh9fMCmNEL4pCGcCiRtSbeuUvXUtMcZNOuUjcdULw/vuPVko57YLH 55 | 8Wd/F3ckIUEVbKlVYHFdl7DGysDQ08lZ2lvbJE+9L4I+emvgpVt33isXav0= 56 | =2hap 57 | -----END PGP PRIVATE KEY BLOCK----- 58 | -------------------------------------------------------------------------------- /test/test_list.py: -------------------------------------------------------------------------------- 1 | """Test list""" 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "location", 10 | [ 11 | "work", 12 | "outside", 13 | "subdir", 14 | ], 15 | ) 16 | @pytest.mark.usefixtures("ds1_copy") 17 | def test_list(runner, yadm_cmd, paths, ds1, location): 18 | """List tests""" 19 | if location == "work": 20 | run_dir = paths.work 21 | elif location == "outside": 22 | run_dir = paths.work.join("..") 23 | else: 24 | assert location == "subdir" 25 | # first directory with tracked data 26 | run_dir = paths.work.join(ds1.tracked_dirs[0]) 27 | with run_dir.as_cwd(): 28 | # test with '-a' 29 | # should get all tracked files, relative to the work path 30 | run = runner(command=yadm_cmd("list", "-a")) 31 | assert run.success 32 | assert run.err == "" 33 | returned_files = set(run.out.splitlines()) 34 | expected_files = {e.path for e in ds1 if e.tracked} 35 | assert returned_files == expected_files 36 | # test without '-a' 37 | # should get all tracked files, relative to the work path unless in a 38 | # subdir, then those should be a limited set of files, relative to the 39 | # subdir 40 | run = runner(command=yadm_cmd("list")) 41 | assert run.success 42 | assert run.err == "" 43 | returned_files = set(run.out.splitlines()) 44 | if location == "subdir": 45 | basepath = os.path.basename(os.getcwd()) 46 | # only expect files within the subdir 47 | # names should be relative to subdir 48 | index = len(basepath) + 1 49 | expected_files = {e.path[index:] for e in ds1 if e.tracked and e.path.startswith(basepath)} 50 | assert returned_files == expected_files 51 | -------------------------------------------------------------------------------- /test/test_perms.py: -------------------------------------------------------------------------------- 1 | """Test perms""" 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | 8 | @pytest.mark.parametrize("autoperms", ["notest", "unset", "true", "false"]) 9 | @pytest.mark.usefixtures("ds1_copy") 10 | def test_perms(runner, yadm_cmd, paths, ds1, autoperms): 11 | """Test perms""" 12 | # set the value of auto-perms 13 | if autoperms != "notest": 14 | if autoperms != "unset": 15 | os.system(" ".join(yadm_cmd("config", "yadm.auto-perms", autoperms))) 16 | 17 | # privatepaths will hold all paths that should become secured 18 | privatepaths = [paths.work.join(".ssh"), paths.work.join(".gnupg")] 19 | privatepaths += [paths.work.join(private.path) for private in ds1.private] 20 | 21 | # create an archive file 22 | os.system(f'touch "{str(paths.archive)}"') 23 | privatepaths.append(paths.archive) 24 | 25 | # create encrypted file test data 26 | efile1 = paths.work.join("efile1") 27 | efile1.write("efile1") 28 | efile2 = paths.work.join("efile2") 29 | efile2.write("efile2") 30 | paths.encrypt.write("efile1\nefile2\n!efile1\n") 31 | insecurepaths = [efile1] 32 | privatepaths.append(efile2) 33 | 34 | # assert these paths begin unsecured 35 | for private in privatepaths + insecurepaths: 36 | assert not oct(private.stat().mode).endswith("00"), "Path started secured" 37 | 38 | cmd = "perms" 39 | if autoperms != "notest": 40 | cmd = "status" 41 | run = runner(yadm_cmd(cmd), env={"HOME": paths.work}) 42 | assert run.success 43 | assert run.err == "" 44 | if cmd == "perms": 45 | assert run.out == "" 46 | 47 | # these paths should be secured if processing perms 48 | for private in privatepaths: 49 | if autoperms == "false": 50 | assert not oct(private.stat().mode).endswith("00"), "Path should not be secured" 51 | else: 52 | assert oct(private.stat().mode).endswith("00"), "Path has not been secured" 53 | 54 | # these paths should never be secured 55 | for private in insecurepaths: 56 | assert not oct(private.stat().mode).endswith("00"), "Path should not be secured" 57 | 58 | 59 | @pytest.mark.parametrize("sshperms", [None, "true", "false"]) 60 | @pytest.mark.parametrize("gpgperms", [None, "true", "false"]) 61 | @pytest.mark.usefixtures("ds1_copy") 62 | def test_perms_control(runner, yadm_cmd, paths, ds1, sshperms, gpgperms): 63 | """Test fine control of perms""" 64 | # set the value of ssh-perms 65 | if sshperms: 66 | os.system(" ".join(yadm_cmd("config", "yadm.ssh-perms", sshperms))) 67 | 68 | # set the value of gpg-perms 69 | if gpgperms: 70 | os.system(" ".join(yadm_cmd("config", "yadm.gpg-perms", gpgperms))) 71 | 72 | # privatepaths will hold all paths that should become secured 73 | privatepaths = [paths.work.join(".ssh"), paths.work.join(".gnupg")] 74 | privatepaths += [paths.work.join(private.path) for private in ds1.private] 75 | 76 | # assert these paths begin unsecured 77 | for private in privatepaths: 78 | assert not oct(private.stat().mode).endswith("00"), "Path started secured" 79 | 80 | run = runner(yadm_cmd("perms"), env={"HOME": paths.work}) 81 | assert run.success 82 | assert run.err == "" 83 | assert run.out == "" 84 | 85 | # these paths should be secured if processing perms 86 | for private in privatepaths: 87 | if (sshperms == "false" and "ssh" in str(private)) or (gpgperms == "false" and "gnupg" in str(private)): 88 | assert not oct(private.stat().mode).endswith("00"), "Path should not be secured" 89 | else: 90 | assert oct(private.stat().mode).endswith("00"), "Path has not been secured" 91 | 92 | # verify permissions aren't changed for the worktree 93 | assert oct(paths.work.stat().mode).endswith("0755") 94 | -------------------------------------------------------------------------------- /test/test_syntax.py: -------------------------------------------------------------------------------- 1 | """Syntax checks""" 2 | 3 | import os 4 | import shutil 5 | 6 | import pytest 7 | 8 | 9 | def test_yadm_syntax(runner, yadm): 10 | """Is syntactically valid""" 11 | run = runner(command=["bash", "-n", yadm]) 12 | assert run.success 13 | 14 | 15 | def test_shellcheck(pytestconfig, runner, yadm, shellcheck_version): 16 | """Passes shellcheck""" 17 | if not pytestconfig.getoption("--force-linters"): 18 | run = runner(command=["shellcheck", "-V"], report=False) 19 | if f"version: {shellcheck_version}" not in run.out: 20 | pytest.skip("Unsupported shellcheck version") 21 | run = runner(command=["shellcheck", "-s", "bash", yadm]) 22 | assert run.success 23 | 24 | 25 | def test_pylint(pytestconfig, runner, pylint_version): 26 | """Passes pylint""" 27 | if not pytestconfig.getoption("--force-linters"): 28 | run = runner(command=["pylint", "--version"], report=False) 29 | if f"pylint {pylint_version}" not in run.out: 30 | pytest.skip("Unsupported pylint version") 31 | pyfiles = [] 32 | for tfile in os.listdir("test"): 33 | if tfile.endswith(".py"): 34 | pyfiles.append(f"test/{tfile}") 35 | run = runner(command=["pylint"] + pyfiles) 36 | assert run.success 37 | 38 | 39 | def test_isort(pytestconfig, runner, isort_version): 40 | """Passes isort""" 41 | if not pytestconfig.getoption("--force-linters"): 42 | run = runner(command=["isort", "--version"], report=False) 43 | if isort_version not in run.out: 44 | pytest.skip("Unsupported isort version") 45 | run = runner(command=["isort", "-c", "test"]) 46 | assert run.success 47 | 48 | 49 | def test_flake8(pytestconfig, runner, flake8_version): 50 | """Passes flake8""" 51 | if not pytestconfig.getoption("--force-linters"): 52 | run = runner(command=["flake8", "--version"], report=False) 53 | if not run.out.startswith(flake8_version): 54 | pytest.skip("Unsupported flake8 version") 55 | run = runner(command=["flake8", "test"]) 56 | assert run.success 57 | 58 | 59 | def test_black(pytestconfig, runner, black_version): 60 | """Passes black""" 61 | if not pytestconfig.getoption("--force-linters"): 62 | run = runner(command=["black", "--version"], report=False) 63 | if black_version not in run.out: 64 | pytest.skip("Unsupported black version") 65 | run = runner(command=["black", "--check", "test"]) 66 | assert run.success 67 | 68 | 69 | def test_yamllint(pytestconfig, runner, yamllint_version): 70 | """Passes yamllint""" 71 | if not pytestconfig.getoption("--force-linters"): 72 | run = runner(command=["yamllint", "--version"], report=False) 73 | if not run.out.strip().endswith(yamllint_version): 74 | pytest.skip("Unsupported yamllint version") 75 | run = runner(command=["yamllint", "-s", "$(find . -name \\*.yml)"], shell=True) 76 | assert run.success 77 | 78 | 79 | def test_man(runner): 80 | """Check for warnings from man""" 81 | if shutil.which("mandoc"): 82 | command = ["mandoc", "-T", "lint"] 83 | else: 84 | command = ["groff", "-ww", "-z"] 85 | run = runner(command=command + ["-man", "./yadm.1"]) 86 | assert run.success 87 | assert run.out == "" 88 | assert run.err == "" 89 | -------------------------------------------------------------------------------- /test/test_unit_bootstrap_available.py: -------------------------------------------------------------------------------- 1 | """Unit tests: bootstrap_available""" 2 | 3 | 4 | def test_bootstrap_missing(runner, paths): 5 | """Test result of bootstrap_available, when bootstrap missing""" 6 | run_test(runner, paths, False) 7 | 8 | 9 | def test_bootstrap_no_exec(runner, paths): 10 | """Test result of bootstrap_available, when bootstrap not executable""" 11 | paths.bootstrap.write("") 12 | paths.bootstrap.chmod(0o644) 13 | run_test(runner, paths, False) 14 | 15 | 16 | def test_bootstrap_exec(runner, paths): 17 | """Test result of bootstrap_available, when bootstrap executable""" 18 | paths.bootstrap.write("") 19 | paths.bootstrap.chmod(0o775) 20 | run_test(runner, paths, True) 21 | 22 | 23 | def run_test(runner, paths, success): 24 | """Run bootstrap_available, and test result""" 25 | script = f""" 26 | YADM_TEST=1 source {paths.pgm} 27 | YADM_BOOTSTRAP='{paths.bootstrap}' 28 | bootstrap_available 29 | """ 30 | run = runner(command=["bash"], inp=script) 31 | assert run.success == success 32 | assert run.err == "" 33 | assert run.out == "" 34 | -------------------------------------------------------------------------------- /test/test_unit_choose_template_processor.py: -------------------------------------------------------------------------------- 1 | """Unit tests: choose_template_processor""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("label", ["", "default", "other"]) 7 | @pytest.mark.parametrize("awk", [True, False], ids=["awk", "no-awk"]) 8 | def test_kind_default(runner, yadm, awk, label): 9 | """Test kind: default""" 10 | 11 | expected = "default" 12 | awk_avail = "true" 13 | 14 | if not awk: 15 | awk_avail = "false" 16 | expected = "" 17 | 18 | if label == "other": 19 | expected = "" 20 | 21 | script = f""" 22 | YADM_TEST=1 source {yadm} 23 | function awk_available {{ {awk_avail}; }} 24 | template="$(choose_template_processor "{label}")" 25 | echo "TEMPLATE:$template" 26 | """ 27 | run = runner(command=["bash"], inp=script) 28 | assert run.success 29 | assert run.err == "" 30 | assert f"TEMPLATE:{expected}\n" in run.out 31 | 32 | 33 | @pytest.mark.parametrize("label", ["envtpl", "j2cli", "j2", "other"]) 34 | @pytest.mark.parametrize("envtpl", [True, False], ids=["envtpl", "no-envtpl"]) 35 | @pytest.mark.parametrize("j2cli", [True, False], ids=["j2cli", "no-j2cli"]) 36 | def test_kind_j2cli_envtpl(runner, yadm, envtpl, j2cli, label): 37 | """Test kind: j2 (both j2cli & envtpl) 38 | 39 | j2cli is preferred over envtpl if available. 40 | """ 41 | 42 | envtpl_avail = "true" if envtpl else "false" 43 | j2cli_avail = "true" if j2cli else "false" 44 | 45 | if label in ("j2cli", "j2") and j2cli: 46 | expected = "j2cli" 47 | elif label in ("envtpl", "j2") and envtpl: 48 | expected = "envtpl" 49 | else: 50 | expected = "" 51 | 52 | script = f""" 53 | YADM_TEST=1 source {yadm} 54 | function envtpl_available {{ {envtpl_avail}; }} 55 | function j2cli_available {{ {j2cli_avail}; }} 56 | template="$(choose_template_processor "{label}")" 57 | echo "TEMPLATE:$template" 58 | """ 59 | run = runner(command=["bash"], inp=script) 60 | assert run.success 61 | assert run.err == "" 62 | assert f"TEMPLATE:{expected}\n" in run.out 63 | -------------------------------------------------------------------------------- /test/test_unit_configure_paths.py: -------------------------------------------------------------------------------- 1 | """Unit tests: configure_paths""" 2 | 3 | import pytest 4 | 5 | ARCHIVE = "archive" 6 | BOOTSTRAP = "bootstrap" 7 | CONFIG = "config" 8 | ENCRYPT = "encrypt" 9 | HOME = "/testhome" 10 | REPO = "repo.git" 11 | YDIR = ".config/yadm" 12 | YDATA = ".local/share/yadm" 13 | 14 | 15 | @pytest.mark.parametrize( 16 | "override, expect", 17 | [ 18 | (None, {}), 19 | ("-Y", {"yadm": "YADM_DIR"}), 20 | ("--yadm-data", {"data": "YADM_DATA"}), 21 | ("--yadm-repo", {"repo": "YADM_REPO", "git": "GIT_DIR"}), 22 | ("--yadm-config", {"config": "YADM_CONFIG"}), 23 | ("--yadm-encrypt", {"encrypt": "YADM_ENCRYPT"}), 24 | ("--yadm-archive", {"archive": "YADM_ARCHIVE"}), 25 | ("--yadm-bootstrap", {"bootstrap": "YADM_BOOTSTRAP"}), 26 | ], 27 | ids=[ 28 | "default", 29 | "override yadm dir", 30 | "override yadm data", 31 | "override repo", 32 | "override config", 33 | "override encrypt", 34 | "override archive", 35 | "override bootstrap", 36 | ], 37 | ) 38 | @pytest.mark.parametrize( 39 | "path", 40 | [".", "./override", "override", ".override", "/override"], 41 | ids=["cwd", "./relative", "relative", "hidden relative", "absolute"], 42 | ) 43 | def test_config(runner, paths, override, expect, path): 44 | """Test configure_paths""" 45 | if path.startswith("/"): 46 | expected_path = path 47 | else: 48 | expected_path = str(paths.root.join(path)) 49 | 50 | args = [override, path] if override else [] 51 | 52 | if override == "-Y": 53 | matches = match_map(expected_path) 54 | elif override == "--yadm-data": 55 | matches = match_map(None, expected_path) 56 | else: 57 | matches = match_map() 58 | 59 | for ekey in expect.keys(): 60 | matches[ekey] = f'{expect[ekey]}="{expected_path}"' 61 | 62 | run_test(runner, paths, args, matches.values(), cwd=str(paths.root)) 63 | 64 | 65 | def match_map(yadm_dir=None, yadm_data=None): 66 | """Create a dictionary of matches, relative to yadm_dir""" 67 | if not yadm_dir: 68 | yadm_dir = "/".join([HOME, YDIR]) 69 | if not yadm_data: 70 | yadm_data = "/".join([HOME, YDATA]) 71 | return { 72 | "yadm": f'YADM_DIR="{yadm_dir}"', 73 | "repo": f'YADM_REPO="{yadm_data}/{REPO}"', 74 | "config": f'YADM_CONFIG="{yadm_dir}/{CONFIG}"', 75 | "encrypt": f'YADM_ENCRYPT="{yadm_dir}/{ENCRYPT}"', 76 | "archive": f'YADM_ARCHIVE="{yadm_data}/{ARCHIVE}"', 77 | "bootstrap": f'YADM_BOOTSTRAP="{yadm_dir}/{BOOTSTRAP}"', 78 | "git": f'GIT_DIR="{yadm_data}/{REPO}"', 79 | } 80 | 81 | 82 | def run_test(runner, paths, args, expected_matches, cwd=None): 83 | """Run proces global args, and run configure_paths""" 84 | argstring = " ".join(['"' + a + '"' for a in args]) 85 | script = f""" 86 | YADM_TEST=1 HOME="{HOME}" source {paths.pgm} 87 | process_global_args {argstring} 88 | XDG_CONFIG_HOME= 89 | XDG_DATA_HOME= 90 | HOME="{HOME}" set_yadm_dirs 91 | configure_paths 92 | for var in "${{!YADM_@}}" "${{!GIT_@}}"; do 93 | echo "$var=\\"${{!var}}\\"" 94 | done 95 | """ 96 | run = runner(command=["bash"], inp=script, cwd=cwd) 97 | assert run.success 98 | assert run.err == "" 99 | for match in expected_matches: 100 | assert match in run.out 101 | -------------------------------------------------------------------------------- /test/test_unit_copy_perms.py: -------------------------------------------------------------------------------- 1 | """Unit tests: copy_perms""" 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | OCTAL = "7654" 8 | NON_OCTAL = "9876" 9 | 10 | 11 | @pytest.mark.parametrize("stat_broken", [False, True], ids=["normal", "stat broken"]) 12 | def test_copy_perms(runner, yadm, tmpdir, stat_broken): 13 | """Test function copy_perms""" 14 | src_mode = 0o754 15 | dst_mode = 0o644 16 | source = tmpdir.join("source") 17 | source.write("test", ensure=True) 18 | source.chmod(src_mode) 19 | 20 | dest = tmpdir.join("dest") 21 | dest.write("test", ensure=True) 22 | dest.chmod(dst_mode) 23 | 24 | override_stat = "" 25 | if stat_broken: 26 | override_stat = "function stat() { echo broken; }" 27 | script = f""" 28 | YADM_TEST=1 source {yadm} 29 | {override_stat} 30 | copy_perms "{source}" "{dest}" 31 | """ 32 | run = runner(command=["bash"], inp=script) 33 | assert run.success 34 | assert run.err == "" 35 | assert run.out == "" 36 | expected = dst_mode if stat_broken else src_mode 37 | assert oct(os.stat(dest).st_mode)[-3:] == oct(expected)[-3:] 38 | 39 | 40 | @pytest.mark.parametrize("stat_output", [OCTAL, NON_OCTAL], ids=["octal", "non-octal"]) 41 | def test_get_mode(runner, yadm, stat_output): 42 | """Test function get_mode""" 43 | script = f""" 44 | YADM_TEST=1 source {yadm} 45 | function stat() {{ echo {stat_output}; }} 46 | mode=$(get_mode abc) 47 | echo "MODE:$mode" 48 | """ 49 | run = runner(command=["bash"], inp=script) 50 | assert run.success 51 | assert run.err == "" 52 | expected = OCTAL if stat_output == OCTAL else "" 53 | assert f"MODE:{expected}\n" in run.out 54 | -------------------------------------------------------------------------------- /test/test_unit_encryption.py: -------------------------------------------------------------------------------- 1 | """Unit tests: encryption functions""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("condition", ["default", "override"]) 7 | def test_get_cipher(runner, paths, condition): 8 | """Test _get_cipher()""" 9 | 10 | if condition == "override": 11 | paths.config.write("[yadm]\n\tcipher = override-cipher") 12 | 13 | script = f""" 14 | YADM_TEST=1 source {paths.pgm} 15 | YADM_DIR="{paths.yadm}" 16 | set_yadm_dirs 17 | configure_paths 18 | _get_cipher test-archive 19 | echo "output_archive:$output_archive" 20 | echo "yadm_cipher:$yadm_cipher" 21 | """ 22 | run = runner(command=["bash"], inp=script) 23 | assert run.success 24 | assert run.err == "" 25 | assert "output_archive:test-archive" in run.out 26 | if condition == "override": 27 | assert "yadm_cipher:override-cipher" in run.out 28 | else: 29 | assert "yadm_cipher:gpg" in run.out 30 | 31 | 32 | @pytest.mark.parametrize("cipher", ["gpg", "openssl", "bad"]) 33 | @pytest.mark.parametrize("mode", ["_encrypt_to", "_decrypt_from"]) 34 | def test_encrypt_decrypt(runner, paths, cipher, mode): 35 | """Test _encrypt_to() & _decrypt_from""" 36 | 37 | script = f""" 38 | YADM_TEST=1 source {paths.pgm} 39 | YADM_DIR="{paths.yadm}" 40 | set_yadm_dirs 41 | configure_paths 42 | function mock_openssl() {{ echo openssl $*; }} 43 | function mock_gpg() {{ echo gpg $*; }} 44 | function _get_cipher() {{ 45 | output_archive="$1" 46 | yadm_cipher="{cipher}" 47 | }} 48 | OPENSSL_PROGRAM=mock_openssl 49 | GPG_PROGRAM=mock_gpg 50 | {mode} {paths.archive} 51 | """ 52 | run = runner(command=["bash"], inp=script) 53 | 54 | if cipher != "bad": 55 | assert run.success 56 | assert run.out.startswith(cipher) 57 | assert str(paths.archive) in run.out 58 | assert run.err == "" 59 | else: 60 | assert run.failure 61 | assert "Unknown cipher" in run.err 62 | 63 | 64 | @pytest.mark.parametrize("condition", ["default", "override"]) 65 | def test_get_openssl_ciphername(runner, paths, condition): 66 | """Test _get_openssl_ciphername()""" 67 | 68 | if condition == "override": 69 | paths.config.write("[yadm]\n\topenssl-ciphername = override-cipher") 70 | 71 | script = f""" 72 | YADM_TEST=1 source {paths.pgm} 73 | YADM_DIR="{paths.yadm}" 74 | set_yadm_dirs 75 | configure_paths 76 | result=$(_get_openssl_ciphername) 77 | echo "result:$result" 78 | """ 79 | run = runner(command=["bash"], inp=script) 80 | assert run.success 81 | assert run.err == "" 82 | if condition == "override": 83 | assert run.out.strip() == "result:override-cipher" 84 | else: 85 | assert run.out.strip() == "result:aes-256-cbc" 86 | 87 | 88 | @pytest.mark.parametrize("condition", ["old", "not-old"]) 89 | def test_set_openssl_options(runner, paths, condition): 90 | """Test _set_openssl_options()""" 91 | 92 | if condition == "old": 93 | paths.config.write("[yadm]\n\topenssl-old = true") 94 | 95 | script = f""" 96 | YADM_TEST=1 source {paths.pgm} 97 | YADM_DIR="{paths.yadm}" 98 | set_yadm_dirs 99 | configure_paths 100 | function _get_openssl_ciphername() {{ echo "testcipher"; }} 101 | _set_openssl_options 102 | echo "result:${{OPENSSL_OPTS[@]}}" 103 | """ 104 | run = runner(command=["bash"], inp=script) 105 | assert run.success 106 | assert run.err == "" 107 | if condition == "old": 108 | assert "-testcipher -salt -md md5" in run.out 109 | else: 110 | assert "-testcipher -salt -pbkdf2 -iter 100000 -md sha512" in run.out 111 | 112 | 113 | @pytest.mark.parametrize("recipient", ["ASK", "present", ""]) 114 | def test_set_gpg_options(runner, paths, recipient): 115 | """Test _set_gpg_options()""" 116 | 117 | paths.config.write(f"[yadm]\n\tgpg-recipient = {recipient}") 118 | 119 | script = f""" 120 | YADM_TEST=1 source {paths.pgm} 121 | YADM_DIR="{paths.yadm}" 122 | set_yadm_dirs 123 | configure_paths 124 | _set_gpg_options 125 | echo "result:${{GPG_OPTS[@]}}" 126 | """ 127 | run = runner(command=["bash"], inp=script) 128 | assert run.success 129 | assert run.err == "" 130 | if recipient == "ASK": 131 | assert run.out.strip() == "result:--no-default-recipient -e" 132 | elif recipient != "": 133 | assert run.out.strip() == f"result:-e -r {recipient}" 134 | else: 135 | assert run.out.strip() == "result:-c" 136 | -------------------------------------------------------------------------------- /test/test_unit_exclude_encrypted.py: -------------------------------------------------------------------------------- 1 | """Unit tests: exclude_encrypted""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("exclude", ["missing", "outdated", "up-to-date"]) 7 | @pytest.mark.parametrize("encrypt_exists", [True, False], ids=["encrypt", "no-encrypt"]) 8 | @pytest.mark.parametrize("auto_exclude", [True, False], ids=["enabled", "disabled"]) 9 | def test_exclude_encrypted(runner, tmpdir, yadm, encrypt_exists, auto_exclude, exclude): 10 | """Test exclude_encrypted()""" 11 | 12 | header = """\ 13 | # yadm-auto-excludes 14 | # This section is managed by yadm. 15 | # Any edits below will be lost. 16 | # yadm encrypt 17 | """ 18 | 19 | config_function = 'function config() { echo "false";}' 20 | if auto_exclude: 21 | config_function = "function config() { return; }" 22 | 23 | encrypt_file = tmpdir.join("encrypt_file") 24 | repo_dir = tmpdir.join("repodir") 25 | exclude_file = repo_dir.join("info/exclude") 26 | 27 | if encrypt_exists: 28 | encrypt_file.write("test-encrypt-data\n", ensure=True) 29 | if exclude == "outdated": 30 | exclude_file.write(f"original-exclude\n{header}outdated\n", ensure=True) 31 | elif exclude == "up-to-date": 32 | exclude_file.write(f"original-exclude\n{header}/test-encrypt-data\n", ensure=True) 33 | 34 | script = f""" 35 | YADM_TEST=1 source {yadm} 36 | {config_function} 37 | DEBUG=1 38 | YADM_ENCRYPT="{encrypt_file}" 39 | YADM_REPO="{repo_dir}" 40 | exclude_encrypted 41 | """ 42 | run = runner(command=["bash"], inp=script) 43 | assert run.success 44 | assert run.err == "" 45 | 46 | if auto_exclude: 47 | if encrypt_exists: 48 | assert exclude_file.exists() 49 | if exclude == "missing": 50 | assert exclude_file.read() == f"{header}/test-encrypt-data\n" 51 | else: 52 | assert exclude_file.read() == ("original-exclude\n" f"{header}/test-encrypt-data\n") 53 | if exclude != "up-to-date": 54 | assert f"Updating {exclude_file}" in run.out 55 | else: 56 | assert run.out == "" 57 | else: 58 | assert run.out == "" 59 | else: 60 | assert run.out == "" 61 | -------------------------------------------------------------------------------- /test/test_unit_issue_legacy_path_warning.py: -------------------------------------------------------------------------------- 1 | """Unit tests: issue_legacy_path_warning""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "legacy_path", 8 | [ 9 | None, 10 | "repo.git", 11 | "files.gpg", 12 | ], 13 | ) 14 | @pytest.mark.parametrize("override", [True, False], ids=["override", "no-override"]) 15 | @pytest.mark.parametrize("upgrade", [True, False], ids=["upgrade", "no-upgrade"]) 16 | def test_legacy_warning(tmpdir, runner, yadm, upgrade, override, legacy_path): 17 | """Use issue_legacy_path_warning""" 18 | home = tmpdir.mkdir("home") 19 | 20 | if legacy_path: 21 | home.ensure(f".config/yadm/{str(legacy_path)}") 22 | 23 | override = "YADM_OVERRIDE_REPO=override" if override else "" 24 | main_args = 'MAIN_ARGS=("upgrade")' if upgrade else "" 25 | script = f""" 26 | XDG_CONFIG_HOME= 27 | XDG_DATA_HOME= 28 | HOME={home} 29 | YADM_TEST=1 source {yadm} 30 | {main_args} 31 | {override} 32 | set_yadm_dirs 33 | issue_legacy_path_warning 34 | """ 35 | run = runner(command=["bash"], inp=script) 36 | assert run.success 37 | assert run.out == "" 38 | if legacy_path and (not upgrade) and (not override): 39 | assert "Legacy paths have been detected" in run.err 40 | else: 41 | assert "Legacy paths have been detected" not in run.err 42 | -------------------------------------------------------------------------------- /test/test_unit_parse_encrypt.py: -------------------------------------------------------------------------------- 1 | """Unit tests: parse_encrypt""" 2 | 3 | import pytest 4 | 5 | 6 | def test_not_called(runner, paths): 7 | """Test parse_encrypt (not called)""" 8 | run = run_parse_encrypt(runner, paths, skip_parse=True) 9 | assert run.success 10 | assert run.err == "" 11 | assert "EIF:unparsed" in run.out, "EIF should be unparsed" 12 | assert "EIF_COUNT:1" in run.out, "Only value of EIF should be unparsed" 13 | 14 | 15 | def test_short_circuit(runner, paths): 16 | """Test parse_encrypt (short-circuit)""" 17 | run = run_parse_encrypt(runner, paths, twice=True) 18 | assert run.success 19 | assert run.err == "" 20 | assert "PARSE_ENCRYPT_SHORT=parse_encrypt() not reprocessed" in run.out, "parse_encrypt() should short-circuit" 21 | 22 | 23 | @pytest.mark.parametrize( 24 | "encrypt", 25 | [ 26 | ("missing"), 27 | ("empty"), 28 | ], 29 | ) 30 | def test_empty(runner, paths, encrypt): 31 | """Test parse_encrypt (file missing/empty)""" 32 | 33 | # write encrypt file 34 | if encrypt == "missing": 35 | assert not paths.encrypt.exists(), "Encrypt should be missing" 36 | else: 37 | paths.encrypt.write("") 38 | assert paths.encrypt.exists(), "Encrypt should exist" 39 | assert paths.encrypt.size() == 0, "Encrypt should be empty" 40 | 41 | # run parse_encrypt 42 | run = run_parse_encrypt(runner, paths) 43 | assert run.success 44 | assert run.err == "" 45 | 46 | # validate parsing result 47 | assert "EIF_COUNT:0" in run.out, "EIF should be empty" 48 | 49 | 50 | def create_test_encrypt_data(paths): 51 | """Generate test data for testing encrypt""" 52 | 53 | edata = "" 54 | expected = set() 55 | 56 | # empty line 57 | edata += "\n" 58 | 59 | # simple comments 60 | edata += "# a simple comment\n" 61 | edata += " # a comment with leading space\n" 62 | 63 | # unreferenced directory 64 | paths.work.join("unreferenced").mkdir() 65 | 66 | # simple files 67 | edata += "simple_file\n" 68 | edata += "simple.file\n" 69 | paths.work.join("simple_file").write("") 70 | paths.work.join("simple.file").write("") 71 | paths.work.join("simple_file2").write("") 72 | paths.work.join("simple.file2").write("") 73 | expected.add("simple_file") 74 | expected.add("simple.file") 75 | 76 | # simple files in directories 77 | edata += "simple_dir/simple_file\n" 78 | paths.work.join("simple_dir/simple_file").write("", ensure=True) 79 | paths.work.join("simple_dir/simple_file2").write("", ensure=True) 80 | expected.add("simple_dir/simple_file") 81 | 82 | # paths with spaces 83 | edata += "with space/with space\n" 84 | paths.work.join("with space/with space").write("", ensure=True) 85 | paths.work.join("with space/with space2").write("", ensure=True) 86 | expected.add("with space/with space") 87 | 88 | # hidden files 89 | edata += ".hidden\n" 90 | paths.work.join(".hidden").write("") 91 | expected.add(".hidden") 92 | 93 | # hidden files in directories 94 | edata += ".hidden_dir/.hidden_file\n" 95 | paths.work.join(".hidden_dir/.hidden_file").write("", ensure=True) 96 | expected.add(".hidden_dir/.hidden_file") 97 | 98 | # wildcards 99 | edata += "wild*\n" 100 | edata += "*card1\n" # matches same file as the one above 101 | paths.work.join("wildcard1").write("", ensure=True) 102 | paths.work.join("wildcard2").write("", ensure=True) 103 | paths.work.join("subdir/wildcard1").write("", ensure=True) 104 | expected.add("wildcard1") 105 | expected.add("wildcard2") 106 | 107 | edata += "dirwild*/file*\n" 108 | paths.work.join("dirwildcard/file1").write("", ensure=True) 109 | paths.work.join("dirwildcard/file2").write("", ensure=True) 110 | expected.add("dirwildcard/file1") 111 | expected.add("dirwildcard/file2") 112 | 113 | # excludes 114 | edata += "exclude*\n" 115 | edata += "ex ex/*\n" 116 | paths.work.join("exclude_file1").write("") 117 | paths.work.join("exclude_file2.ex").write("") 118 | paths.work.join("exclude_file3.ex3").write("") 119 | expected.add("exclude_file1") 120 | expected.add("exclude_file3.ex3") 121 | edata += "!*.ex\n" 122 | edata += "!ex ex/*.txt\n" 123 | paths.work.join("ex ex/file4").write("", ensure=True) 124 | paths.work.join("ex ex/file5.txt").write("", ensure=True) 125 | paths.work.join("ex ex/file6.text").write("", ensure=True) 126 | expected.add("ex ex/file4") 127 | expected.add("ex ex/file6.text") 128 | 129 | paths.work.join("dirwildcard/file7.ex").write("", ensure=True) 130 | expected.add("dirwildcard/file7.ex") 131 | 132 | # double star 133 | edata += "doublestar/**/file*\n" 134 | edata += "!**/file3\n" 135 | paths.work.join("doublestar/a/b/file1").write("", ensure=True) 136 | paths.work.join("doublestar/c/d/file2").write("", ensure=True) 137 | paths.work.join("doublestar/e/f/file3").write("", ensure=True) 138 | paths.work.join("doublestar/g/h/nomatch").write("", ensure=True) 139 | expected.add("doublestar/a/b/file1") 140 | expected.add("doublestar/c/d/file2") 141 | # doublestar/e/f/file3 is excluded 142 | 143 | return edata, expected 144 | 145 | 146 | @pytest.mark.usefixtures("ds1_repo_copy") 147 | def test_file_parse_encrypt(runner, paths): 148 | """Test parse_encrypt 149 | 150 | Test an array of supported features of the encrypt configuration. 151 | """ 152 | 153 | # generate test data & expectations 154 | edata, expected = create_test_encrypt_data(paths) 155 | 156 | # write encrypt file 157 | print(f"ENCRYPT:\n---\n{edata}---\n") 158 | paths.encrypt.write(edata) 159 | assert paths.encrypt.isfile() 160 | 161 | # run parse_encrypt 162 | run = run_parse_encrypt(runner, paths) 163 | assert run.success 164 | assert run.err == "" 165 | 166 | assert f"EIF_COUNT:{len(expected)}" in run.out, "EIF count wrong" 167 | for expected_file in expected: 168 | assert f"EIF:{expected_file}\n" in run.out 169 | 170 | sorted_expectations = "\n".join([f"EIF:{exp}" for exp in sorted(expected)]) 171 | assert sorted_expectations in run.out 172 | 173 | 174 | def run_parse_encrypt(runner, paths, skip_parse=False, twice=False): 175 | """Run parse_encrypt 176 | 177 | A count of ENCRYPT_INCLUDE_FILES will be reported as EIF_COUNT:X. All 178 | values of ENCRYPT_INCLUDE_FILES will be reported as individual EIF:value 179 | lines. 180 | """ 181 | parse_cmd = "parse_encrypt" 182 | if skip_parse: 183 | parse_cmd = "" 184 | if twice: 185 | parse_cmd = "parse_encrypt; parse_encrypt" 186 | script = f""" 187 | YADM_TEST=1 source {paths.pgm} 188 | YADM_ENCRYPT={paths.encrypt} 189 | export YADM_ENCRYPT 190 | GIT_DIR={paths.repo} 191 | export GIT_DIR 192 | YADM_WORK={paths.work} 193 | export YADM_WORK 194 | {parse_cmd} 195 | echo PARSE_ENCRYPT_SHORT=$PARSE_ENCRYPT_SHORT 196 | echo EIF_COUNT:${{#ENCRYPT_INCLUDE_FILES[@]}} 197 | for value in "${{ENCRYPT_INCLUDE_FILES[@]}}"; do 198 | echo "EIF:$value" 199 | done 200 | """ 201 | run = runner(command=["bash"], inp=script) 202 | return run 203 | -------------------------------------------------------------------------------- /test/test_unit_private_dirs.py: -------------------------------------------------------------------------------- 1 | """Unit tests: private_dirs""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "gnupghome", 8 | [True, False], 9 | ids=["gnupghome-set", "gnupghome-unset"], 10 | ) 11 | @pytest.mark.parametrize("param", ["all", "gnupg"]) 12 | def test_relative_path(runner, paths, gnupghome, param): 13 | """Test translate_to_relative""" 14 | 15 | alt_gnupghome = "alt/gnupghome" 16 | env_gnupghome = paths.work.join(alt_gnupghome) 17 | 18 | script = f""" 19 | YADM_TEST=1 source {paths.pgm} 20 | YADM_WORK={paths.work} 21 | private_dirs {param} 22 | """ 23 | 24 | env = {} 25 | if gnupghome: 26 | env["GNUPGHOME"] = env_gnupghome 27 | 28 | expected = alt_gnupghome if gnupghome else ".gnupg" 29 | if param == "all": 30 | expected = f".ssh {expected}" 31 | 32 | run = runner(command=["bash"], inp=script, env=env) 33 | assert run.success 34 | assert run.err == "" 35 | assert run.out.strip() == expected 36 | -------------------------------------------------------------------------------- /test/test_unit_query_distro.py: -------------------------------------------------------------------------------- 1 | """Unit tests: query_distro""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("condition", ["lsb_release", "os-release", "os-release-quotes", "missing"]) 7 | def test_query_distro(runner, yadm, tst_distro, tmp_path, condition): 8 | """Match lsb_release -si when present""" 9 | test_release = "testrelease" 10 | lsb_release = "" 11 | os_release = tmp_path.joinpath("os-release") 12 | if "os-release" in condition: 13 | quotes = '"' if "quotes" in condition else "" 14 | os_release.write_text(f"testing\nID={quotes}{test_release}{quotes}\nrelease") 15 | if condition != "lsb_release": 16 | lsb_release = 'LSB_RELEASE_PROGRAM="missing_lsb_release"' 17 | script = f""" 18 | YADM_TEST=1 source {yadm} 19 | {lsb_release} 20 | OS_RELEASE="{os_release}" 21 | query_distro 22 | """ 23 | run = runner(command=["bash"], inp=script) 24 | assert run.success 25 | assert run.err == "" 26 | if condition == "lsb_release": 27 | assert run.out.rstrip() == tst_distro 28 | elif "os-release" in condition: 29 | assert run.out.rstrip() == test_release 30 | else: 31 | assert run.out.rstrip() == "" 32 | -------------------------------------------------------------------------------- /test/test_unit_query_distro_family.py: -------------------------------------------------------------------------------- 1 | """Unit tests: query_distro_family""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("condition", ["os-release", "os-release-quotes", "missing", "fallback"]) 7 | def test_query_distro_family(runner, yadm, tmp_path, condition): 8 | """Match ID_LIKE when present""" 9 | test_family = "testfamily" 10 | os_release = tmp_path.joinpath("os-release") 11 | if "os-release" in condition: 12 | quotes = '"' if "quotes" in condition else "" 13 | os_release.write_text(f"testing\nID=test\nID_LIKE={quotes}{test_family}{quotes}\nfamily") 14 | elif condition == "fallback": 15 | os_release.write_text(f'testing\nID="{test_family}"\nfamily') 16 | script = f""" 17 | YADM_TEST=1 source {yadm} 18 | OS_RELEASE="{os_release}" 19 | query_distro_family 20 | """ 21 | run = runner(command=["bash"], inp=script) 22 | assert run.success 23 | assert run.err == "" 24 | if condition == "missing": 25 | assert run.out.rstrip() == "" 26 | else: 27 | assert run.out.rstrip() == test_family 28 | -------------------------------------------------------------------------------- /test/test_unit_record_score.py: -------------------------------------------------------------------------------- 1 | """Unit tests: record_score""" 2 | 3 | import pytest 4 | 5 | INIT_VARS = """ 6 | score=0 7 | local_class=testclass 8 | local_system=testsystem 9 | local_host=testhost 10 | local_user=testuser 11 | alt_scores=() 12 | alt_targets=() 13 | alt_sources=() 14 | alt_template_processors=() 15 | """ 16 | 17 | REPORT_RESULTS = """ 18 | echo "SIZE:${#alt_scores[@]}" 19 | echo "SCORES:${alt_scores[@]}" 20 | echo "TARGETS:${alt_targets[@]}" 21 | echo "SOURCES:${alt_sources[@]}" 22 | echo "TEMPLATE_PROCESSORS:${alt_template_processors[@]}" 23 | """ 24 | 25 | 26 | def test_dont_record_zeros(runner, yadm): 27 | """Record nothing if the score is zero""" 28 | 29 | script = f""" 30 | YADM_TEST=1 source {yadm} 31 | {INIT_VARS} 32 | record_score "0" "testtgt" "testsrc" 33 | {REPORT_RESULTS} 34 | """ 35 | run = runner(command=["bash"], inp=script) 36 | assert run.success 37 | assert run.err == "" 38 | assert "SIZE:0\n" in run.out 39 | assert "SCORES:\n" in run.out 40 | assert "TARGETS:\n" in run.out 41 | assert "SOURCES:\n" in run.out 42 | assert "TEMPLATE_PROCESSORS:\n" in run.out 43 | 44 | 45 | def test_new_scores(runner, yadm): 46 | """Test new scores""" 47 | 48 | script = f""" 49 | YADM_TEST=1 source {yadm} 50 | {INIT_VARS} 51 | record_score "1" "tgt_one" "src_one" "" 52 | record_score "2" "tgt_two" "src_two" "" 53 | record_score "4" "tgt_three" "src_three" "" 54 | {REPORT_RESULTS} 55 | """ 56 | run = runner(command=["bash"], inp=script) 57 | assert run.success 58 | assert run.err == "" 59 | assert "SIZE:3\n" in run.out 60 | assert "SCORES:1 2 4\n" in run.out 61 | assert "TARGETS:tgt_one tgt_two tgt_three\n" in run.out 62 | assert "SOURCES:src_one src_two src_three\n" in run.out 63 | assert "TEMPLATE_PROCESSORS: \n" in run.out 64 | 65 | 66 | @pytest.mark.parametrize("difference", ["lower", "equal", "higher"]) 67 | def test_existing_scores(runner, yadm, difference): 68 | """Test existing scores""" 69 | 70 | expected_score = "2" 71 | expected_src = "existing_src" 72 | if difference == "lower": 73 | score = "1" 74 | elif difference == "equal": 75 | score = "2" 76 | else: 77 | score = "4" 78 | expected_score = "4" 79 | expected_src = "new_src" 80 | 81 | script = f""" 82 | YADM_TEST=1 source {yadm} 83 | {INIT_VARS} 84 | alt_scores=(2) 85 | alt_targets=("testtgt") 86 | alt_sources=("existing_src") 87 | alt_template_processors=("") 88 | record_score "{score}" "testtgt" "new_src" "" 89 | {REPORT_RESULTS} 90 | """ 91 | run = runner(command=["bash"], inp=script) 92 | assert run.success 93 | assert run.err == "" 94 | assert "SIZE:1\n" in run.out 95 | assert f"SCORES:{expected_score}\n" in run.out 96 | assert "TARGETS:testtgt\n" in run.out 97 | assert f"SOURCES:{expected_src}\n" in run.out 98 | assert "TEMPLATE_PROCESSORS:\n" in run.out 99 | 100 | 101 | def test_existing_template(runner, yadm): 102 | """Record nothing if a template command is registered for this target""" 103 | 104 | script = f""" 105 | YADM_TEST=1 source {yadm} 106 | {INIT_VARS} 107 | alt_scores=(1) 108 | alt_targets=("testtgt") 109 | alt_sources=("src") 110 | alt_template_processors=("existing_template") 111 | record_score "2" "testtgt" "new_src" "" 112 | {REPORT_RESULTS} 113 | """ 114 | run = runner(command=["bash"], inp=script) 115 | assert run.success 116 | assert run.err == "" 117 | assert "SIZE:1\n" in run.out 118 | assert "SCORES:1\n" in run.out 119 | assert "TARGETS:testtgt\n" in run.out 120 | assert "SOURCES:src\n" in run.out 121 | assert "TEMPLATE_PROCESSORS:existing_template\n" in run.out 122 | 123 | 124 | def test_config_first(runner, yadm): 125 | """Verify YADM_CONFIG is always processed first""" 126 | 127 | config = "yadm_config_file" 128 | script = f""" 129 | YADM_TEST=1 source {yadm} 130 | {INIT_VARS} 131 | YADM_CONFIG={config} 132 | record_score "1" "tgt_before" "src_before" "" 133 | record_score "1" "tgt_tmp" "src_tmp" "processor_tmp" 134 | record_score "2" "{config}" "src_config" "" 135 | record_score "3" "tgt_after" "src_after" "" 136 | {REPORT_RESULTS} 137 | """ 138 | run = runner(command=["bash"], inp=script) 139 | assert run.success 140 | assert run.err == "" 141 | assert "SIZE:4\n" in run.out 142 | assert "SCORES:2 1 1 3\n" in run.out 143 | assert f"TARGETS:{config} tgt_before tgt_tmp tgt_after\n" in run.out 144 | assert "SOURCES:src_config src_before src_tmp src_after\n" in run.out 145 | assert "TEMPLATE_PROCESSORS: processor_tmp \n" in run.out 146 | 147 | 148 | def test_new_template(runner, yadm): 149 | """Test new template""" 150 | 151 | script = f""" 152 | YADM_TEST=1 source {yadm} 153 | {INIT_VARS} 154 | record_score 0 "tgt_one" "src_one" "processor_one" 155 | record_score 0 "tgt_two" "src_two" "processor_two" 156 | record_score 0 "tgt_three" "src_three" "processor_three" 157 | {REPORT_RESULTS} 158 | """ 159 | run = runner(command=["bash"], inp=script) 160 | assert run.success 161 | assert run.err == "" 162 | assert "SIZE:3\n" in run.out 163 | assert "SCORES:0 0 0\n" in run.out 164 | assert "TARGETS:tgt_one tgt_two tgt_three\n" in run.out 165 | assert "SOURCES:src_one src_two src_three\n" in run.out 166 | assert "TEMPLATE_PROCESSORS:processor_one processor_two processor_three\n" in run.out 167 | 168 | 169 | def test_overwrite_existing_template(runner, yadm): 170 | """Overwrite existing templates""" 171 | 172 | script = f""" 173 | YADM_TEST=1 source {yadm} 174 | {INIT_VARS} 175 | alt_scores=(0) 176 | alt_targets=("testtgt") 177 | alt_template_processors=("existing_processor") 178 | alt_sources=("existing_src") 179 | record_score 0 "testtgt" "new_src" "new_processor" 180 | {REPORT_RESULTS} 181 | """ 182 | run = runner(command=["bash"], inp=script) 183 | assert run.success 184 | assert run.err == "" 185 | assert "SIZE:1\n" in run.out 186 | assert "SCORES:0\n" in run.out 187 | assert "TARGETS:testtgt\n" in run.out 188 | assert "SOURCES:new_src\n" in run.out 189 | assert "TEMPLATE_PROCESSORS:new_processor\n" in run.out 190 | -------------------------------------------------------------------------------- /test/test_unit_relative_path.py: -------------------------------------------------------------------------------- 1 | """Unit tests: relative_path""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "base,full_path,expected", 8 | [ 9 | ("/A/B/C", "/A", "../.."), 10 | ("/A/B/C", "/A/B", ".."), 11 | ("/A/B/C", "/A/B/C", ""), 12 | ("/A/B/C", "/A/B/C/D", "D"), 13 | ("/A/B/C", "/A/B/C/D/E", "D/E"), 14 | ("/A/B/C", "/A/B/CD", "../CD"), 15 | ("/A/B/C", "/A/BB/C", "../../BB/C"), 16 | ("/A/B/C", "/A/B/D", "../D"), 17 | ("/A/B/C", "/A/B/D/E", "../D/E"), 18 | ("/A/B/C", "/A/D", "../../D"), 19 | ("/A/B/C", "/A/D/E", "../../D/E"), 20 | ("/A/B/C", "/D/E/F", "../../../D/E/F"), 21 | ("/", "/A/B/C", "A/B/C"), 22 | ("/A/B/C", "/", "../../.."), 23 | ("/A/B B/C", "/A/C C/D", "../../C C/D"), 24 | ], 25 | ) 26 | def test_relative_path(runner, paths, base, full_path, expected): 27 | """Test translate_to_relative""" 28 | 29 | script = f""" 30 | YADM_TEST=1 source {paths.pgm} 31 | relative_path "{base}" "{full_path}" 32 | """ 33 | 34 | run = runner(command=["bash"], inp=script) 35 | assert run.success 36 | assert run.err == "" 37 | assert run.out.strip() == expected 38 | -------------------------------------------------------------------------------- /test/test_unit_report_invalid_alts.py: -------------------------------------------------------------------------------- 1 | """Unit tests: report_invalid_alts""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("valid", [True, False], ids=["valid", "no_valid"]) 7 | @pytest.mark.parametrize("previous", [True, False], ids=["prev", "no_prev"]) 8 | def test_report_invalid_alts(runner, yadm, valid, previous): 9 | """Use report_invalid_alts""" 10 | 11 | lwi = "" 12 | alts = "INVALID_ALT=()" 13 | if previous: 14 | lwi = "LEGACY_WARNING_ISSUED=1" 15 | if not valid: 16 | alts = 'INVALID_ALT=("file##invalid")' 17 | 18 | script = f""" 19 | YADM_TEST=1 source {yadm} 20 | {lwi} 21 | {alts} 22 | report_invalid_alts 23 | """ 24 | run = runner(command=["bash"], inp=script) 25 | assert run.success 26 | assert run.out == "" 27 | if not valid and not previous: 28 | assert "WARNING" in run.err 29 | assert "file##invalid" in run.err 30 | else: 31 | assert run.err == "" 32 | -------------------------------------------------------------------------------- /test/test_unit_score_file.py: -------------------------------------------------------------------------------- 1 | """Unit tests: score_file""" 2 | 3 | import pytest 4 | 5 | CONDITION = { 6 | "default": { 7 | "labels": ["default"], 8 | "modifier": 0, 9 | }, 10 | "arch": { 11 | "labels": ["a", "arch"], 12 | "modifier": 1, 13 | }, 14 | "system": { 15 | "labels": ["o", "os"], 16 | "modifier": 2, 17 | }, 18 | "distro": { 19 | "labels": ["d", "distro"], 20 | "modifier": 4, 21 | }, 22 | "distro_family": { 23 | "labels": ["f", "distro_family"], 24 | "modifier": 8, 25 | }, 26 | "class": { 27 | "labels": ["c", "class"], 28 | "modifier": 16, 29 | }, 30 | "hostname": { 31 | "labels": ["h", "hostname"], 32 | "modifier": 32, 33 | }, 34 | "user": { 35 | "labels": ["u", "user"], 36 | "modifier": 64, 37 | }, 38 | } 39 | TEMPLATE_LABELS = ["t", "template", "yadm"] 40 | 41 | 42 | def calculate_score(conditions): 43 | """Calculate the expected score""" 44 | # pylint: disable=too-many-branches 45 | score = 0 46 | 47 | for condition in conditions.split(","): 48 | label = condition 49 | value = None 50 | if "." in condition: 51 | label, value = condition.split(".", 1) 52 | if label in CONDITION["default"]["labels"]: 53 | score += 1000 54 | elif label in CONDITION["arch"]["labels"]: 55 | if value.lower() == "testarch": 56 | score += 1000 + CONDITION["arch"]["modifier"] 57 | else: 58 | score = 0 59 | break 60 | elif label in CONDITION["system"]["labels"]: 61 | if value.lower() == "testsystem": 62 | score += 1000 + CONDITION["system"]["modifier"] 63 | else: 64 | score = 0 65 | break 66 | elif label in CONDITION["distro"]["labels"]: 67 | if value.lower() == "testdistro": 68 | score += 1000 + CONDITION["distro"]["modifier"] 69 | else: 70 | score = 0 71 | break 72 | elif label in CONDITION["class"]["labels"]: 73 | if value.lower() == "testclass": 74 | score += 1000 + CONDITION["class"]["modifier"] 75 | else: 76 | score = 0 77 | break 78 | elif label in CONDITION["hostname"]["labels"]: 79 | if value.lower() == "testhost": 80 | score += 1000 + CONDITION["hostname"]["modifier"] 81 | else: 82 | score = 0 83 | break 84 | elif label in CONDITION["user"]["labels"]: 85 | if value.lower() == "testuser": 86 | score += 1000 + CONDITION["user"]["modifier"] 87 | else: 88 | score = 0 89 | break 90 | elif label not in TEMPLATE_LABELS: 91 | score = 0 92 | break 93 | return score 94 | 95 | 96 | @pytest.mark.parametrize("default", ["default", None], ids=["default", "no-default"]) 97 | @pytest.mark.parametrize("arch", ["arch", None], ids=["arch", "no-arch"]) 98 | @pytest.mark.parametrize("system", ["system", None], ids=["system", "no-system"]) 99 | @pytest.mark.parametrize("distro", ["distro", None], ids=["distro", "no-distro"]) 100 | @pytest.mark.parametrize("cla", ["class", None], ids=["class", "no-class"]) 101 | @pytest.mark.parametrize("host", ["hostname", None], ids=["hostname", "no-host"]) 102 | @pytest.mark.parametrize("user", ["user", None], ids=["user", "no-user"]) 103 | def test_score_values(runner, yadm, default, arch, system, distro, cla, host, user): 104 | """Test score results""" 105 | # pylint: disable=too-many-branches 106 | local_class = "testClass" 107 | local_arch = "testARch" 108 | local_system = "TESTsystem" 109 | local_distro = "testDISTro" 110 | local_host = "testHost" 111 | local_user = "testUser" 112 | conditions = {"": 0} 113 | 114 | if default: 115 | for condition in list(conditions): 116 | for label in CONDITION[default]["labels"]: 117 | newcond = condition 118 | if newcond: 119 | newcond += "," 120 | newcond += label 121 | conditions[newcond] = calculate_score(newcond) 122 | if arch: 123 | for condition in list(conditions): 124 | for match in [True, False]: 125 | for label in CONDITION[arch]["labels"]: 126 | newcond = condition 127 | if newcond: 128 | newcond += "," 129 | newcond += ".".join([label, local_arch if match else "badarch"]) 130 | conditions[newcond] = calculate_score(newcond) 131 | if system: 132 | for condition in list(conditions): 133 | for match in [True, False]: 134 | for label in CONDITION[system]["labels"]: 135 | newcond = condition 136 | if newcond: 137 | newcond += "," 138 | newcond += ".".join([label, local_system if match else "badsys"]) 139 | conditions[newcond] = calculate_score(newcond) 140 | if distro: 141 | for condition in list(conditions): 142 | for match in [True, False]: 143 | for label in CONDITION[distro]["labels"]: 144 | newcond = condition 145 | if newcond: 146 | newcond += "," 147 | newcond += ".".join([label, local_distro if match else "baddistro"]) 148 | conditions[newcond] = calculate_score(newcond) 149 | if cla: 150 | for condition in list(conditions): 151 | for match in [True, False]: 152 | for label in CONDITION[cla]["labels"]: 153 | newcond = condition 154 | if newcond: 155 | newcond += "," 156 | newcond += ".".join([label, local_class if match else "badclass"]) 157 | conditions[newcond] = calculate_score(newcond) 158 | if host: 159 | for condition in list(conditions): 160 | for match in [True, False]: 161 | for label in CONDITION[host]["labels"]: 162 | newcond = condition 163 | if newcond: 164 | newcond += "," 165 | newcond += ".".join([label, local_host if match else "badhost"]) 166 | conditions[newcond] = calculate_score(newcond) 167 | if user: 168 | for condition in list(conditions): 169 | for match in [True, False]: 170 | for label in CONDITION[user]["labels"]: 171 | newcond = condition 172 | if newcond: 173 | newcond += "," 174 | newcond += ".".join([label, local_user if match else "baduser"]) 175 | conditions[newcond] = calculate_score(newcond) 176 | 177 | script = f""" 178 | YADM_TEST=1 source {yadm} 179 | score=0 180 | local_class={local_class} 181 | local_classes=({local_class}) 182 | local_arch={local_arch} 183 | local_system={local_system} 184 | local_distro={local_distro} 185 | local_host={local_host} 186 | local_user={local_user} 187 | """ 188 | expected = [] 189 | for condition, score in conditions.items(): 190 | script += f""" 191 | score_file "source" "target" "{condition}" 192 | echo "{condition}=$score" 193 | """ 194 | expected.append(f"{condition}={score}") 195 | expected.append("") 196 | expected = "\n".join(expected) 197 | run = runner(command=["bash"], inp=script) 198 | assert run.success 199 | assert run.err == "" 200 | assert run.out == expected 201 | 202 | 203 | @pytest.mark.parametrize("ext", [None, "e", "extension"]) 204 | def test_extensions(runner, yadm, ext): 205 | """Verify extensions do not effect scores""" 206 | local_user = "testuser" 207 | condition = f"u.{local_user}" 208 | if ext: 209 | condition += f",{ext}.xyz" 210 | expected = "" 211 | script = f""" 212 | YADM_TEST=1 source {yadm} 213 | score=0 214 | local_user={local_user} 215 | score_file "source" "target" "{condition}" 216 | echo "$score" 217 | """ 218 | expected = f'{1000 + CONDITION["user"]["modifier"]}\n' 219 | run = runner(command=["bash"], inp=script) 220 | assert run.success 221 | assert run.err == "" 222 | assert run.out == expected 223 | 224 | 225 | def test_score_values_templates(runner, yadm): 226 | """Test score results""" 227 | local_class = "testclass" 228 | local_arch = "arch" 229 | local_system = "testsystem" 230 | local_distro = "testdistro" 231 | local_host = "testhost" 232 | local_user = "testuser" 233 | conditions = {"": 0} 234 | 235 | for condition in list(conditions): 236 | for label in TEMPLATE_LABELS: 237 | newcond = condition 238 | if newcond: 239 | newcond += "," 240 | newcond += ".".join([label, "testtemplate"]) 241 | conditions[newcond] = calculate_score(newcond) 242 | 243 | script = f""" 244 | YADM_TEST=1 source {yadm} 245 | score=0 246 | local_class={local_class} 247 | local_arch={local_arch} 248 | local_system={local_system} 249 | local_distro={local_distro} 250 | local_host={local_host} 251 | local_user={local_user} 252 | """ 253 | expected = [] 254 | for condition, score in conditions.items(): 255 | script += f""" 256 | score_file "source" "target" "{condition}" 257 | echo "{condition}=$score" 258 | """ 259 | expected.append(f"{condition}={score}") 260 | expected.append("") 261 | expected = "\n".join(expected) 262 | run = runner(command=["bash"], inp=script) 263 | assert run.success 264 | assert run.err == "" 265 | assert run.out == expected 266 | 267 | 268 | @pytest.mark.parametrize("processor_generated", [True, False], ids=["supported-template", "unsupported-template"]) 269 | def test_template_recording(runner, yadm, processor_generated): 270 | """Template should be recorded if choose_template_processor outputs a command""" 271 | 272 | mock = "function choose_template_processor() { return; }" 273 | expected = "" 274 | if processor_generated: 275 | mock = 'function choose_template_processor() { echo "test_processor"; }' 276 | expected = "template recorded" 277 | 278 | script = f""" 279 | YADM_TEST=1 source {yadm} 280 | function record_score() {{ [ -n "$4" ] && echo "template recorded"; }} 281 | {mock} 282 | score_file "source" "target" "template.kind" 283 | """ 284 | run = runner(command=["bash"], inp=script) 285 | assert run.success 286 | assert run.err == "" 287 | assert run.out.rstrip() == expected 288 | 289 | 290 | def test_underscores_and_upper_case_in_distro_and_family(runner, yadm): 291 | """Test replacing spaces with underscores and lowering case in distro / distro_family""" 292 | local_distro = "test distro" 293 | local_distro_family = "test family" 294 | conditions = { 295 | "distro.Test Distro": 1004, 296 | "distro.test-distro": 0, 297 | "distro.test_distro": 1004, 298 | "distro_family.test FAMILY": 1008, 299 | "distro_family.test-family": 0, 300 | "distro_family.test_family": 1008, 301 | } 302 | 303 | script = f""" 304 | YADM_TEST=1 source {yadm} 305 | score=0 306 | local_distro="{local_distro}" 307 | local_distro_family="{local_distro_family}" 308 | """ 309 | expected = [] 310 | for condition, score in conditions.items(): 311 | script += f""" 312 | score_file "source" "target" "{condition}" 313 | echo "{condition}=$score" 314 | """ 315 | expected.append(f"{condition}={score}") 316 | expected.append("") 317 | expected = "\n".join(expected) 318 | run = runner(command=["bash"], inp=script) 319 | assert run.success 320 | assert run.err == "" 321 | assert run.out == expected 322 | 323 | 324 | def test_negative_class_condition(runner, yadm): 325 | """Test negative class condition: returns 0 when matching and proper score when not matching.""" 326 | script = f""" 327 | YADM_TEST=1 source {yadm} 328 | local_class="testclass" 329 | local_classes=("testclass") 330 | 331 | # 0 332 | score=0 333 | score_file "source" "target" "~class.testclass" 334 | echo "score: $score" 335 | 336 | # 16 337 | score=0 338 | score_file "source" "target" "~class.badclass" 339 | echo "score2: $score" 340 | 341 | # 16 342 | score=0 343 | score_file "source" "target" "~c.badclass" 344 | echo "score3: $score" 345 | """ 346 | run = runner(command=["bash"], inp=script) 347 | assert run.success 348 | output = run.out.strip().splitlines() 349 | assert output[0] == "score: 0" 350 | assert output[1] == "score2: 16" 351 | assert output[2] == "score3: 16" 352 | 353 | 354 | def test_negative_combined_conditions(runner, yadm): 355 | """Test negative conditions for multiple alt types: returns 0 when matching and proper score when not matching.""" 356 | script = f""" 357 | YADM_TEST=1 source {yadm} 358 | local_class="testclass" 359 | local_classes=("testclass") 360 | local_distro="testdistro" 361 | 362 | # (0) + (0) = 0 363 | score=0 364 | score_file "source" "target" "~class.testclass,~distro.testdistro" 365 | echo "score: $score" 366 | 367 | # (1000 + 16) + (1000 + 4) = 2020 368 | score=0 369 | score_file "source" "target" "class.testclass,distro.testdistro" 370 | echo "score2: $score" 371 | 372 | # 0 (negated class condition) 373 | score=0 374 | score_file "source" "target" "~class.badclass,~distro.testdistro" 375 | echo "score3: $score" 376 | 377 | # (1000 + 16) + (4) = 1020 378 | score=0 379 | score_file "source" "target" "class.testclass,~distro.baddistro" 380 | echo "score4: $score" 381 | 382 | # (1000 + 16) + (16) = 1032 383 | score=0 384 | score_file "source" "target" "class.testclass,~class.badclass" 385 | echo "score5: $score" 386 | """ 387 | run = runner(command=["bash"], inp=script) 388 | assert run.success 389 | output = run.out.strip().splitlines() 390 | assert output[0] == "score: 0" 391 | assert output[1] == "score2: 2020" 392 | assert output[2] == "score3: 0" 393 | assert output[3] == "score4: 1020" 394 | assert output[4] == "score5: 1032" 395 | -------------------------------------------------------------------------------- /test/test_unit_set_local_alt_values.py: -------------------------------------------------------------------------------- 1 | """Unit tests: set_local_alt_values""" 2 | 3 | import pytest 4 | import utils 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "override", 9 | [ 10 | False, 11 | "class", 12 | "arch", 13 | "os", 14 | "hostname", 15 | "user", 16 | "distro", 17 | "distro-family", 18 | ], 19 | ids=[ 20 | "no-override", 21 | "override-class", 22 | "override-arch", 23 | "override-os", 24 | "override-hostname", 25 | "override-user", 26 | "override-distro", 27 | "override-distro-family", 28 | ], 29 | ) 30 | @pytest.mark.usefixtures("ds1_copy") 31 | def test_set_local_alt_values( 32 | runner, yadm, paths, tst_arch, tst_sys, tst_host, tst_user, tst_distro, tst_distro_family, override 33 | ): 34 | """Test handling of local alt values""" 35 | script = f""" 36 | YADM_TEST=1 source {yadm} && 37 | set_operating_system && 38 | YADM_DIR={paths.yadm} YADM_DATA={paths.data} configure_paths && 39 | set_local_alt_values 40 | echo "class='$local_class'" 41 | echo "arch='$local_arch'" 42 | echo "os='$local_system'" 43 | echo "hostname='$local_host'" 44 | echo "user='$local_user'" 45 | echo "distro='$local_distro'" 46 | echo "distro-family='$local_distro_family'" 47 | """ 48 | 49 | if override == "class": 50 | utils.set_local(paths, override, "first") 51 | utils.set_local(paths, override, "override", add=True) 52 | elif override: 53 | utils.set_local(paths, override, "override") 54 | 55 | run = runner(command=["bash"], inp=script) 56 | assert run.success 57 | assert run.err == "" 58 | 59 | default_values = { 60 | "class": "", 61 | "arch": tst_arch, 62 | "os": tst_sys, 63 | "hostname": tst_host, 64 | "user": tst_user, 65 | "distro": tst_distro, 66 | "distro-family": tst_distro_family, 67 | } 68 | 69 | for key, value in default_values.items(): 70 | if key == override: 71 | assert f"{key}='override'" in run.out 72 | else: 73 | assert f"{key}='{value}'" in run.out 74 | -------------------------------------------------------------------------------- /test/test_unit_set_os.py: -------------------------------------------------------------------------------- 1 | """Unit tests: set_operating_system""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "proc_value, expected_os", 8 | [ 9 | ("missing", "uname"), 10 | ("has microsoft inside", "WSL"), # case insensitive 11 | ("has Microsoft inside", "WSL"), # case insensitive 12 | ("another value", "uname"), 13 | ], 14 | ids=[ 15 | "/proc/version missing", 16 | "/proc/version includes ms", 17 | "/proc/version excludes Ms", 18 | "another value", 19 | ], 20 | ) 21 | def test_set_operating_system(runner, paths, tst_sys, proc_value, expected_os): 22 | """Run set_operating_system and test result""" 23 | 24 | # Normally /proc/version (set in PROC_VERSION) is inspected to identify 25 | # WSL. During testing, we will override that value. 26 | proc_version = paths.root.join("proc_version") 27 | if proc_value != "missing": 28 | proc_version.write(proc_value) 29 | script = f""" 30 | YADM_TEST=1 source {paths.pgm} 31 | PROC_VERSION={proc_version} 32 | set_operating_system 33 | echo $OPERATING_SYSTEM 34 | """ 35 | run = runner(command=["bash"], inp=script) 36 | assert run.success 37 | assert run.err == "" 38 | if expected_os == "uname": 39 | expected_os = tst_sys if tst_sys != "WSL" else "Linux" 40 | assert run.out.rstrip() == expected_os 41 | -------------------------------------------------------------------------------- /test/test_unit_set_yadm_dir.py: -------------------------------------------------------------------------------- 1 | """Unit tests: set_yadm_dirs""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "condition", 8 | ["basic", "override", "override_data", "xdg_config_home", "xdg_data_home"], 9 | ) 10 | def test_set_yadm_dirs(runner, yadm, condition): 11 | """Test set_yadm_dirs""" 12 | setup = "" 13 | if condition == "override": 14 | setup = "YADM_DIR=/override" 15 | elif condition == "override_data": 16 | setup = "YADM_DATA=/override" 17 | elif condition == "xdg_config_home": 18 | setup = "XDG_CONFIG_HOME=/xdg" 19 | elif condition == "xdg_data_home": 20 | setup = "XDG_DATA_HOME=/xdg" 21 | script = f""" 22 | HOME=/testhome 23 | YADM_TEST=1 source {yadm} 24 | XDG_CONFIG_HOME= 25 | XDG_DATA_HOME= 26 | {setup} 27 | set_yadm_dirs 28 | echo "YADM_DIR=$YADM_DIR" 29 | echo "YADM_DATA=$YADM_DATA" 30 | """ 31 | run = runner(command=["bash"], inp=script) 32 | assert run.success 33 | assert run.err == "" 34 | if condition == "basic": 35 | assert "YADM_DIR=/testhome/.config/yadm" in run.out 36 | assert "YADM_DATA=/testhome/.local/share/yadm" in run.out 37 | elif condition == "override": 38 | assert "YADM_DIR=/override" in run.out 39 | elif condition == "override_data": 40 | assert "YADM_DATA=/override" in run.out 41 | elif condition == "xdg_config_home": 42 | assert "YADM_DIR=/xdg/yadm" in run.out 43 | elif condition == "xdg_data_home": 44 | assert "YADM_DATA=/xdg/yadm" in run.out 45 | -------------------------------------------------------------------------------- /test/test_unit_template_default.py: -------------------------------------------------------------------------------- 1 | """Unit tests: template_default""" 2 | 3 | import os 4 | 5 | FILE_MODE = 0o754 6 | 7 | # these values are also testing the handling of bizarre characters 8 | LOCAL_CLASS = "default_Test+@-!^Class" 9 | LOCAL_CLASS2 = "default_Test+@-|^2nd_Class withSpace" 10 | LOCAL_ARCH = "default_Test+@-!^Arch" 11 | LOCAL_SYSTEM = "default_Test+@-!^System" 12 | LOCAL_HOST = "default_Test+@-!^Host" 13 | LOCAL_USER = "default_Test+@-!^User" 14 | LOCAL_DISTRO = "default_Test+@-!^Distro" 15 | LOCAL_DISTRO_FAMILY = "default_Test+@-!^Family" 16 | ENV_VAR = "default_Test+@-!^Env" 17 | TEMPLATE = f""" 18 | start of template 19 | default class = >{{{{yadm.class}}}}< 20 | default arch = >{{{{yadm.arch}}}}< 21 | default os = >{{{{yadm.os}}}}< 22 | default host = >{{{{yadm.hostname}}}}< 23 | default user = >{{{{yadm.user}}}}< 24 | default distro = >{{{{yadm.distro}}}}< 25 | default distro_family = >{{{{yadm.distro_family}}}}< 26 | classes = >{{{{yadm.classes}}}}< 27 | {{% if yadm.class == "else1" %}} 28 | wrong else 1 29 | {{% else %}} 30 | Included section from else 31 | {{% endif %}} 32 | {{% if yadm.class == "wrongclass1" %}} 33 | wrong class 1 34 | {{% endif %}} 35 | {{% if yadm.class != "wronglcass" %}} 36 | Included section from != 37 | {{% endif\t\t %}} 38 | {{% if yadm.class == "{LOCAL_CLASS.lower()}" %}} 39 | Included section for class = {{{{yadm.class}}}} ({{{{yadm.class}}}} repeated) 40 | Multiple lines 41 | {{% else %}} 42 | Should not be included... 43 | {{% endif %}} 44 | {{% if yadm.class == "{LOCAL_CLASS2.upper()}" %}} 45 | Included section for second class 46 | {{% endif %}} 47 | {{% if yadm.class == "wrongclass2" %}} 48 | wrong class 2 49 | {{% endif %}} 50 | {{% if yadm.arch == "wrongarch1" %}} 51 | wrong arch 1 52 | {{% endif %}} 53 | {{% if yadm.arch == "{LOCAL_ARCH.title()}" %}} 54 | Included section for arch = {{{{yadm.arch}}}} ({{{{yadm.arch}}}} repeated) 55 | {{% endif %}} 56 | {{% if yadm.arch == "wrongarch2" %}} 57 | wrong arch 2 58 | {{% endif %}} 59 | {{% if yadm.os == "wrongos1" %}} 60 | wrong os 1 61 | {{% endif %}} 62 | {{% if yadm.os == "{LOCAL_SYSTEM.lower()}" %}} 63 | Included section for os = {{{{yadm.os}}}} ({{{{yadm.os}}}} repeated) 64 | {{% endif %}} 65 | {{% if yadm.os == "wrongos2" %}} 66 | wrong os 2 67 | {{% endif %}} 68 | {{% if yadm.hostname == "wronghost1" %}} 69 | wrong host 1 70 | {{% endif %}} 71 | {{% if yadm.hostname == "{LOCAL_HOST.upper()}" %}} 72 | Included section for host = {{{{yadm.hostname}}}} ({{{{yadm.hostname}}}} again) 73 | {{% endif %}} 74 | {{% if yadm.hostname == "wronghost2" %}} 75 | wrong host 2 76 | {{% endif %}} 77 | {{% if yadm.user == "wronguser1" %}} 78 | wrong user 1 79 | {{% endif %}} 80 | {{% if yadm.user == "{LOCAL_USER.title()}" %}} 81 | Included section for user = {{{{yadm.user}}}} ({{{{yadm.user}}}} repeated) 82 | {{% endif %}} 83 | {{% if yadm.user == "wronguser2" %}} 84 | wrong user 2 85 | {{% endif %}} 86 | {{% if yadm.distro == "wrongdistro1" %}} 87 | wrong distro 1 88 | {{% endif %}} 89 | {{% if yadm.distro == "{LOCAL_DISTRO.lower()}" %}} 90 | Included section for distro = {{{{yadm.distro}}}} ({{{{yadm.distro}}}} again) 91 | {{% endif %}} 92 | {{% if yadm.distro == "wrongdistro2" %}} 93 | wrong distro 2 94 | {{% endif %}} 95 | {{% if yadm.distro_family == "wrongfamily1" %}} 96 | wrong family 1 97 | {{% endif %}} 98 | {{% if yadm.distro_family == "{LOCAL_DISTRO_FAMILY.upper()}" %}} 99 | Included section for distro_family = \ 100 | {{{{yadm.distro_family}}}} ({{{{yadm.distro_family}}}} again) 101 | {{% endif %}} 102 | {{% if yadm.distro_family == "wrongfamily2" %}} 103 | wrong family 2 104 | {{% endif %}} 105 | {{% if env.VAR == "{ENV_VAR.title()}" %}} 106 | Included section for env.VAR = {{{{env.VAR}}}} ({{{{env.VAR}}}} again) 107 | {{% endif %}} 108 | {{% if env.VAR == "wrongenvvar" %}} 109 | wrong env.VAR 110 | {{% endif %}} 111 | yadm.no_such_var="{{{{ yadm.no_such_var }}}}" and env.NO_SUCH_VAR="{{{{ env.NO_SUCH_VAR }}}}" 112 | end of template 113 | """ 114 | EXPECTED = f""" 115 | start of template 116 | default class = >{LOCAL_CLASS}< 117 | default arch = >{LOCAL_ARCH}< 118 | default os = >{LOCAL_SYSTEM}< 119 | default host = >{LOCAL_HOST}< 120 | default user = >{LOCAL_USER}< 121 | default distro = >{LOCAL_DISTRO}< 122 | default distro_family = >{LOCAL_DISTRO_FAMILY}< 123 | classes = >{LOCAL_CLASS2} 124 | {LOCAL_CLASS}< 125 | Included section from else 126 | Included section from != 127 | Included section for class = {LOCAL_CLASS} ({LOCAL_CLASS} repeated) 128 | Multiple lines 129 | Included section for second class 130 | Included section for arch = {LOCAL_ARCH} ({LOCAL_ARCH} repeated) 131 | Included section for os = {LOCAL_SYSTEM} ({LOCAL_SYSTEM} repeated) 132 | Included section for host = {LOCAL_HOST} ({LOCAL_HOST} again) 133 | Included section for user = {LOCAL_USER} ({LOCAL_USER} repeated) 134 | Included section for distro = {LOCAL_DISTRO} ({LOCAL_DISTRO} again) 135 | Included section for distro_family = \ 136 | {LOCAL_DISTRO_FAMILY} ({LOCAL_DISTRO_FAMILY} again) 137 | Included section for env.VAR = {ENV_VAR} ({ENV_VAR} again) 138 | yadm.no_such_var="" and env.NO_SUCH_VAR="" 139 | end of template 140 | """ 141 | 142 | INCLUDE_BASIC = "basic\n" 143 | INCLUDE_VARIABLES = """\ 144 | included <{{ yadm.class }}> file ({{yadm.filename}}) 145 | 146 | empty line above 147 | """ 148 | INCLUDE_NESTED = "no newline at the end" 149 | 150 | TEMPLATE_INCLUDE = """\ 151 | The first line 152 | {% include empty %} 153 | An empty file removes the line above 154 | {%include ./basic%} 155 | {% include "variables.{{ yadm.os }}" %} 156 | {% include dir/nested %} 157 | Include basic again: 158 | {% include basic %} 159 | """ 160 | EXPECTED_INCLUDE = f"""\ 161 | The first line 162 | An empty file removes the line above 163 | basic 164 | included <{LOCAL_CLASS}> file (VARIABLES_FILENAME) 165 | 166 | empty line above 167 | no newline at the end 168 | Include basic again: 169 | basic 170 | """ 171 | 172 | TEMPLATE_NESTED_IFS = """\ 173 | {% if yadm.user == "me" %} 174 | print1 175 | {% if yadm.user == "me" %} 176 | print2 177 | {% else %} 178 | no print1 179 | {% endif %} 180 | {% else %} 181 | {% if yadm.user == "me" %} 182 | no print2 183 | {% else %} 184 | no print3 185 | {% endif %} 186 | {% endif %} 187 | {% if yadm.user != "me" %} 188 | no print4 189 | {% if yadm.user == "me" %} 190 | no print5 191 | {% else %} 192 | no print6 193 | {% endif %} 194 | {% else %} 195 | {% if yadm.user == "me" %} 196 | print3 197 | {% else %} 198 | no print7 199 | {% endif %} 200 | {% endif %} 201 | """ 202 | EXPECTED_NESTED_IFS = """\ 203 | print1 204 | print2 205 | print3 206 | """ 207 | 208 | 209 | def test_template_default(runner, yadm, tmpdir): 210 | """Test template_default""" 211 | 212 | input_file = tmpdir.join("input") 213 | input_file.write(TEMPLATE, ensure=True) 214 | input_file.chmod(FILE_MODE) 215 | output_file = tmpdir.join("output") 216 | 217 | # ensure overwrite works when file exists as read-only (there is some 218 | # special processing when this is encountered because some environments do 219 | # not properly overwrite read-only files) 220 | output_file.write("existing") 221 | output_file.chmod(0o400) 222 | 223 | script = f""" 224 | YADM_TEST=1 source {yadm} 225 | set_awk 226 | local_class="{LOCAL_CLASS}" 227 | local_classes=("{LOCAL_CLASS2}" "{LOCAL_CLASS}") 228 | local_arch="{LOCAL_ARCH}" 229 | local_system="{LOCAL_SYSTEM}" 230 | local_host="{LOCAL_HOST}" 231 | local_user="{LOCAL_USER}" 232 | local_distro="{LOCAL_DISTRO}" 233 | local_distro_family="{LOCAL_DISTRO_FAMILY}" 234 | template default "{input_file}" "{output_file}" 235 | """ 236 | run = runner(command=["bash"], inp=script, env={"VAR": ENV_VAR}) 237 | assert run.success 238 | assert run.err == "" 239 | assert output_file.read() == EXPECTED 240 | assert os.stat(output_file).st_mode == os.stat(input_file).st_mode 241 | 242 | 243 | def test_source(runner, yadm, tmpdir): 244 | """Test yadm.source""" 245 | 246 | input_file = tmpdir.join("input") 247 | input_file.write("{{yadm.source}}", ensure=True) 248 | input_file.chmod(FILE_MODE) 249 | output_file = tmpdir.join("output") 250 | 251 | script = f""" 252 | YADM_TEST=1 source {yadm} 253 | set_awk 254 | template default "{input_file}" "{output_file}" 255 | """ 256 | run = runner(command=["bash"], inp=script) 257 | assert run.success 258 | assert run.err == "" 259 | assert output_file.read().strip() == str(input_file) 260 | assert os.stat(output_file).st_mode == os.stat(input_file).st_mode 261 | 262 | 263 | def test_include(runner, yadm, tmpdir): 264 | """Test include""" 265 | 266 | empty_file = tmpdir.join("empty") 267 | empty_file.write("", ensure=True) 268 | 269 | basic_file = tmpdir.join("basic") 270 | basic_file.write(INCLUDE_BASIC) 271 | 272 | variables_file = tmpdir.join(f"variables.{LOCAL_SYSTEM}") 273 | variables_file.write(INCLUDE_VARIABLES) 274 | 275 | nested_file = tmpdir.join("dir").join("nested") 276 | nested_file.write(INCLUDE_NESTED, ensure=True) 277 | 278 | input_file = tmpdir.join("input") 279 | input_file.write(TEMPLATE_INCLUDE) 280 | input_file.chmod(FILE_MODE) 281 | output_file = tmpdir.join("output") 282 | 283 | expected = EXPECTED_INCLUDE.replace("VARIABLES_FILENAME", str(variables_file)) 284 | 285 | script = f""" 286 | YADM_TEST=1 source {yadm} 287 | set_awk 288 | local_class="{LOCAL_CLASS}" 289 | local_system="{LOCAL_SYSTEM}" 290 | template default "{input_file}" "{output_file}" 291 | """ 292 | run = runner(command=["bash"], inp=script) 293 | assert run.success 294 | assert run.err == "" 295 | assert output_file.read() == expected 296 | assert os.stat(output_file).st_mode == os.stat(input_file).st_mode 297 | 298 | 299 | def test_nested_ifs(runner, yadm, tmpdir): 300 | """Test nested if statements""" 301 | 302 | input_file = tmpdir.join("input") 303 | input_file.write(TEMPLATE_NESTED_IFS, ensure=True) 304 | output_file = tmpdir.join("output") 305 | 306 | script = f""" 307 | YADM_TEST=1 source {yadm} 308 | set_awk 309 | local_user="me" 310 | template default "{input_file}" "{output_file}" 311 | """ 312 | run = runner(command=["bash"], inp=script) 313 | assert run.success 314 | assert run.err == "" 315 | assert output_file.read() == EXPECTED_NESTED_IFS 316 | 317 | 318 | def test_env(runner, yadm, tmpdir): 319 | """Test env""" 320 | 321 | input_file = tmpdir.join("input") 322 | input_file.write("{{env.PWD}}", ensure=True) 323 | output_file = tmpdir.join("output") 324 | 325 | script = f""" 326 | YADM_TEST=1 source {yadm} 327 | set_awk 328 | template default "{input_file}" "{output_file}" 329 | """ 330 | run = runner(command=["bash"], inp=script) 331 | assert run.success 332 | assert run.err == "" 333 | assert output_file.read().strip() == os.environ["PWD"] 334 | -------------------------------------------------------------------------------- /test/test_unit_template_esh.py: -------------------------------------------------------------------------------- 1 | """Unit tests: template_esh""" 2 | 3 | import os 4 | 5 | FILE_MODE = 0o754 6 | 7 | LOCAL_CLASS = "esh_Test+@-!^Class" 8 | LOCAL_CLASS2 = "esh_Test+@-|^2nd_Class withSpace" 9 | LOCAL_ARCH = "esh_Test+@-!^Arch" 10 | LOCAL_SYSTEM = "esh_Test+@-!^System" 11 | LOCAL_HOST = "esh_Test+@-!^Host" 12 | LOCAL_USER = "esh_Test+@-!^User" 13 | LOCAL_DISTRO = "esh_Test+@-!^Distro" 14 | LOCAL_DISTRO_FAMILY = "esh_Test+@-!^Family" 15 | TEMPLATE = f""" 16 | start of template 17 | esh class = ><%=$YADM_CLASS%>< 18 | esh arch = ><%=$YADM_ARCH%>< 19 | esh os = ><%=$YADM_OS%>< 20 | esh host = ><%=$YADM_HOSTNAME%>< 21 | esh user = ><%=$YADM_USER%>< 22 | esh distro = ><%=$YADM_DISTRO%>< 23 | esh distro_family = ><%=$YADM_DISTRO_FAMILY%>< 24 | esh classes = ><%=$YADM_CLASSES%>< 25 | <% if [ "$YADM_CLASS" = "wrongclass1" ]; then -%> 26 | wrong class 1 27 | <% fi -%> 28 | <% if [ "$YADM_CLASS" = "{LOCAL_CLASS}" ]; then -%> 29 | Included esh section for class = <%=$YADM_CLASS%> (<%=$YADM_CLASS%> repeated) 30 | <% fi -%> 31 | <% if [ "$YADM_CLASS" = "wrongclass2" ]; then -%> 32 | wrong class 2 33 | <% fi -%> 34 | <% echo "$YADM_CLASSES" | while IFS='' read cls; do 35 | if [ "$cls" = "{LOCAL_CLASS2}" ]; then -%> 36 | Included esh section for second class 37 | <% fi; done -%> 38 | <% if [ "$YADM_ARCH" = "wrongarch1" ]; then -%> 39 | wrong arch 1 40 | <% fi -%> 41 | <% if [ "$YADM_ARCH" = "{LOCAL_ARCH}" ]; then -%> 42 | Included esh section for arch = <%=$YADM_ARCH%> (<%=$YADM_ARCH%> repeated) 43 | <% fi -%> 44 | <% if [ "$YADM_ARCH" = "wrongarch2" ]; then -%> 45 | wrong arch 2 46 | <% fi -%> 47 | <% if [ "$YADM_OS" = "wrongos1" ]; then -%> 48 | wrong os 1 49 | <% fi -%> 50 | <% if [ "$YADM_OS" = "{LOCAL_SYSTEM}" ]; then -%> 51 | Included esh section for os = <%=$YADM_OS%> (<%=$YADM_OS%> repeated) 52 | <% fi -%> 53 | <% if [ "$YADM_OS" = "wrongos2" ]; then -%> 54 | wrong os 2 55 | <% fi -%> 56 | <% if [ "$YADM_HOSTNAME" = "wronghost1" ]; then -%> 57 | wrong host 1 58 | <% fi -%> 59 | <% if [ "$YADM_HOSTNAME" = "{LOCAL_HOST}" ]; then -%> 60 | Included esh section for host = <%=$YADM_HOSTNAME%> (<%=$YADM_HOSTNAME%> again) 61 | <% fi -%> 62 | <% if [ "$YADM_HOSTNAME" = "wronghost2" ]; then -%> 63 | wrong host 2 64 | <% fi -%> 65 | <% if [ "$YADM_USER" = "wronguser1" ]; then -%> 66 | wrong user 1 67 | <% fi -%> 68 | <% if [ "$YADM_USER" = "{LOCAL_USER}" ]; then -%> 69 | Included esh section for user = <%=$YADM_USER%> (<%=$YADM_USER%> repeated) 70 | <% fi -%> 71 | <% if [ "$YADM_USER" = "wronguser2" ]; then -%> 72 | wrong user 2 73 | <% fi -%> 74 | <% if [ "$YADM_DISTRO" = "wrongdistro1" ]; then -%> 75 | wrong distro 1 76 | <% fi -%> 77 | <% if [ "$YADM_DISTRO" = "{LOCAL_DISTRO}" ]; then -%> 78 | Included esh section for distro = <%=$YADM_DISTRO%> (<%=$YADM_DISTRO%> again) 79 | <% fi -%> 80 | <% if [ "$YADM_DISTRO" = "wrongdistro2" ]; then -%> 81 | wrong distro 2 82 | <% fi -%> 83 | <% if [ "$YADM_DISTRO_FAMILY" = "wrongfamily1" ]; then -%> 84 | wrong family 1 85 | <% fi -%> 86 | <% if [ "$YADM_DISTRO_FAMILY" = "{LOCAL_DISTRO_FAMILY}" ]; then -%> 87 | Included esh section for distro_family = \ 88 | <%=$YADM_DISTRO_FAMILY%> (<%=$YADM_DISTRO_FAMILY%> again) 89 | <% fi -%> 90 | <% if [ "$YADM_DISTRO" = "wrongfamily2" ]; then -%> 91 | wrong family 2 92 | <% fi -%> 93 | end of template 94 | """ 95 | EXPECTED = f""" 96 | start of template 97 | esh class = >{LOCAL_CLASS}< 98 | esh arch = >{LOCAL_ARCH}< 99 | esh os = >{LOCAL_SYSTEM}< 100 | esh host = >{LOCAL_HOST}< 101 | esh user = >{LOCAL_USER}< 102 | esh distro = >{LOCAL_DISTRO}< 103 | esh distro_family = >{LOCAL_DISTRO_FAMILY}< 104 | esh classes = >{LOCAL_CLASS2} {LOCAL_CLASS}< 105 | Included esh section for class = {LOCAL_CLASS} ({LOCAL_CLASS} repeated) 106 | Included esh section for second class 107 | Included esh section for arch = {LOCAL_ARCH} ({LOCAL_ARCH} repeated) 108 | Included esh section for os = {LOCAL_SYSTEM} ({LOCAL_SYSTEM} repeated) 109 | Included esh section for host = {LOCAL_HOST} ({LOCAL_HOST} again) 110 | Included esh section for user = {LOCAL_USER} ({LOCAL_USER} repeated) 111 | Included esh section for distro = {LOCAL_DISTRO} ({LOCAL_DISTRO} again) 112 | Included esh section for distro_family = \ 113 | {LOCAL_DISTRO_FAMILY} ({LOCAL_DISTRO_FAMILY} again) 114 | end of template 115 | """ 116 | 117 | 118 | def test_template_esh(runner, yadm, tmpdir): 119 | """Test processing by esh""" 120 | # pylint: disable=duplicate-code 121 | 122 | input_file = tmpdir.join("input") 123 | input_file.write(TEMPLATE, ensure=True) 124 | input_file.chmod(FILE_MODE) 125 | output_file = tmpdir.join("output") 126 | 127 | # ensure overwrite works when file exists as read-only (there is some 128 | # special processing when this is encountered because some environments do 129 | # not properly overwrite read-only files) 130 | output_file.write("existing") 131 | output_file.chmod(0o400) 132 | 133 | script = f""" 134 | YADM_TEST=1 source {yadm} 135 | local_class="{LOCAL_CLASS}" 136 | local_classes=("{LOCAL_CLASS2}" "{LOCAL_CLASS}") 137 | local_arch="{LOCAL_ARCH}" 138 | local_system="{LOCAL_SYSTEM}" 139 | local_host="{LOCAL_HOST}" 140 | local_user="{LOCAL_USER}" 141 | local_distro="{LOCAL_DISTRO}" 142 | local_distro_family="{LOCAL_DISTRO_FAMILY}" 143 | template esh "{input_file}" "{output_file}" 144 | """ 145 | run = runner(command=["bash"], inp=script) 146 | assert run.success 147 | assert run.err == "" 148 | assert output_file.read().strip() == str(EXPECTED).strip() 149 | assert os.stat(output_file).st_mode == os.stat(input_file).st_mode 150 | 151 | 152 | def test_source(runner, yadm, tmpdir): 153 | """Test YADM_SOURCE""" 154 | 155 | input_file = tmpdir.join("input") 156 | input_file.write("<%= $YADM_SOURCE %>", ensure=True) 157 | input_file.chmod(FILE_MODE) 158 | output_file = tmpdir.join("output") 159 | 160 | script = f""" 161 | YADM_TEST=1 source {yadm} 162 | template esh "{input_file}" "{output_file}" 163 | """ 164 | run = runner(command=["bash"], inp=script) 165 | assert run.success 166 | assert run.err == "" 167 | assert output_file.read().strip() == str(input_file) 168 | assert os.stat(output_file).st_mode == os.stat(input_file).st_mode 169 | -------------------------------------------------------------------------------- /test/test_unit_template_j2.py: -------------------------------------------------------------------------------- 1 | """Unit tests: template_j2cli & template_envtpl""" 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | FILE_MODE = 0o754 8 | 9 | LOCAL_CLASS = "j2_Test+@-!^Class" 10 | LOCAL_CLASS2 = "j2_Test+@-|^2nd_Class withSpace" 11 | LOCAL_ARCH = "j2_Test+@-!^Arch" 12 | LOCAL_SYSTEM = "j2_Test+@-!^System" 13 | LOCAL_HOST = "j2_Test+@-!^Host" 14 | LOCAL_USER = "j2_Test+@-!^User" 15 | LOCAL_DISTRO = "j2_Test+@-!^Distro" 16 | LOCAL_DISTRO_FAMILY = "j2_Test+@-!^Family" 17 | TEMPLATE = f""" 18 | start of template 19 | j2 class = >{{{{YADM_CLASS}}}}< 20 | j2 arch = >{{{{YADM_ARCH}}}}< 21 | j2 os = >{{{{YADM_OS}}}}< 22 | j2 host = >{{{{YADM_HOSTNAME}}}}< 23 | j2 user = >{{{{YADM_USER}}}}< 24 | j2 distro = >{{{{YADM_DISTRO}}}}< 25 | j2 distro_family = >{{{{YADM_DISTRO_FAMILY}}}}< 26 | j2 classes = >{{{{YADM_CLASSES}}}}< 27 | {{%- if YADM_CLASS == "wrongclass1" %}} 28 | wrong class 1 29 | {{%- endif %}} 30 | {{%- if YADM_CLASS == "{LOCAL_CLASS}" %}} 31 | Included j2 section for class = \ 32 | {{{{YADM_CLASS}}}} ({{{{YADM_CLASS}}}} repeated) 33 | {{%- endif %}} 34 | {{%- if YADM_CLASS == "wrongclass2" %}} 35 | wrong class 2 36 | {{%- endif %}} 37 | {{%- if "{LOCAL_CLASS2}" in YADM_CLASSES.split("\\n") %}} 38 | Included j2 section for second class 39 | {{%- endif %}} 40 | {{%- if YADM_ARCH == "wrongarch1" %}} 41 | wrong arch 1 42 | {{%- endif %}} 43 | {{%- if YADM_ARCH == "{LOCAL_ARCH}" %}} 44 | Included j2 section for arch = {{{{YADM_ARCH}}}} ({{{{YADM_ARCH}}}} repeated) 45 | {{%- endif %}} 46 | {{%- if YADM_ARCH == "wrongarch2" %}} 47 | wrong arch 2 48 | {{%- endif %}} 49 | {{%- if YADM_OS == "wrongos1" %}} 50 | wrong os 1 51 | {{%- endif %}} 52 | {{%- if YADM_OS == "{LOCAL_SYSTEM}" %}} 53 | Included j2 section for os = {{{{YADM_OS}}}} ({{{{YADM_OS}}}} repeated) 54 | {{%- endif %}} 55 | {{%- if YADM_OS == "wrongos2" %}} 56 | wrong os 2 57 | {{%- endif %}} 58 | {{%- if YADM_HOSTNAME == "wronghost1" %}} 59 | wrong host 1 60 | {{%- endif %}} 61 | {{%- if YADM_HOSTNAME == "{LOCAL_HOST}" %}} 62 | Included j2 section for host = \ 63 | {{{{YADM_HOSTNAME}}}} ({{{{YADM_HOSTNAME}}}} again) 64 | {{%- endif %}} 65 | {{%- if YADM_HOSTNAME == "wronghost2" %}} 66 | wrong host 2 67 | {{%- endif %}} 68 | {{%- if YADM_USER == "wronguser1" %}} 69 | wrong user 1 70 | {{%- endif %}} 71 | {{%- if YADM_USER == "{LOCAL_USER}" %}} 72 | Included j2 section for user = {{{{YADM_USER}}}} ({{{{YADM_USER}}}} repeated) 73 | {{%- endif %}} 74 | {{%- if YADM_USER == "wronguser2" %}} 75 | wrong user 2 76 | {{%- endif %}} 77 | {{%- if YADM_DISTRO == "wrongdistro1" %}} 78 | wrong distro 1 79 | {{%- endif %}} 80 | {{%- if YADM_DISTRO == "{LOCAL_DISTRO}" %}} 81 | Included j2 section for distro = \ 82 | {{{{YADM_DISTRO}}}} ({{{{YADM_DISTRO}}}} again) 83 | {{%- endif %}} 84 | {{%- if YADM_DISTRO == "wrongdistro2" %}} 85 | wrong distro 2 86 | {{%- endif %}} 87 | {{%- if YADM_DISTRO_FAMILY == "wrongfamily1" %}} 88 | wrong family 1 89 | {{%- endif %}} 90 | {{%- if YADM_DISTRO_FAMILY == "{LOCAL_DISTRO_FAMILY}" %}} 91 | Included j2 section for distro_family = \ 92 | {{{{YADM_DISTRO_FAMILY}}}} ({{{{YADM_DISTRO_FAMILY}}}} again) 93 | {{%- endif %}} 94 | {{%- if YADM_DISTRO_FAMILY == "wrongfamily2" %}} 95 | wrong family 2 96 | {{%- endif %}} 97 | end of template 98 | """ 99 | EXPECTED = f""" 100 | start of template 101 | j2 class = >{LOCAL_CLASS}< 102 | j2 arch = >{LOCAL_ARCH}< 103 | j2 os = >{LOCAL_SYSTEM}< 104 | j2 host = >{LOCAL_HOST}< 105 | j2 user = >{LOCAL_USER}< 106 | j2 distro = >{LOCAL_DISTRO}< 107 | j2 distro_family = >{LOCAL_DISTRO_FAMILY}< 108 | j2 classes = >{LOCAL_CLASS2} 109 | {LOCAL_CLASS}< 110 | Included j2 section for class = {LOCAL_CLASS} ({LOCAL_CLASS} repeated) 111 | Included j2 section for second class 112 | Included j2 section for arch = {LOCAL_ARCH} ({LOCAL_ARCH} repeated) 113 | Included j2 section for os = {LOCAL_SYSTEM} ({LOCAL_SYSTEM} repeated) 114 | Included j2 section for host = {LOCAL_HOST} ({LOCAL_HOST} again) 115 | Included j2 section for user = {LOCAL_USER} ({LOCAL_USER} repeated) 116 | Included j2 section for distro = {LOCAL_DISTRO} ({LOCAL_DISTRO} again) 117 | Included j2 section for distro_family = \ 118 | {LOCAL_DISTRO_FAMILY} ({LOCAL_DISTRO_FAMILY} again) 119 | end of template 120 | """ 121 | 122 | 123 | @pytest.mark.parametrize("processor", ("j2cli", "envtpl")) 124 | def test_template_j2(runner, yadm, tmpdir, processor): 125 | """Test processing by j2cli & envtpl""" 126 | # pylint: disable=duplicate-code 127 | 128 | input_file = tmpdir.join("input") 129 | input_file.write(TEMPLATE, ensure=True) 130 | input_file.chmod(FILE_MODE) 131 | output_file = tmpdir.join("output") 132 | 133 | # ensure overwrite works when file exists as read-only (there is some 134 | # special processing when this is encountered because some environments do 135 | # not properly overwrite read-only files) 136 | output_file.write("existing") 137 | output_file.chmod(0o400) 138 | 139 | script = f""" 140 | YADM_TEST=1 source {yadm} 141 | local_class="{LOCAL_CLASS}" 142 | local_classes=("{LOCAL_CLASS2}" "{LOCAL_CLASS}") 143 | local_arch="{LOCAL_ARCH}" 144 | local_system="{LOCAL_SYSTEM}" 145 | local_host="{LOCAL_HOST}" 146 | local_user="{LOCAL_USER}" 147 | local_distro="{LOCAL_DISTRO}" 148 | local_distro_family="{LOCAL_DISTRO_FAMILY}" 149 | template {processor} "{input_file}" "{output_file}" 150 | """ 151 | run = runner(command=["bash"], inp=script) 152 | assert run.success 153 | assert run.err == "" 154 | assert output_file.read() == EXPECTED 155 | assert os.stat(output_file).st_mode == os.stat(input_file).st_mode 156 | 157 | 158 | @pytest.mark.parametrize("processor", ("j2cli", "envtpl")) 159 | def test_source(runner, yadm, tmpdir, processor): 160 | """Test YADM_SOURCE""" 161 | 162 | input_file = tmpdir.join("input") 163 | input_file.write("{{YADM_SOURCE}}", ensure=True) 164 | input_file.chmod(FILE_MODE) 165 | output_file = tmpdir.join("output") 166 | 167 | script = f""" 168 | YADM_TEST=1 source {yadm} 169 | template {processor} "{input_file}" "{output_file}" 170 | """ 171 | run = runner(command=["bash"], inp=script) 172 | assert run.success 173 | assert run.err == "" 174 | assert output_file.read().strip() == str(input_file) 175 | assert os.stat(output_file).st_mode == os.stat(input_file).st_mode 176 | -------------------------------------------------------------------------------- /test/test_unit_upgrade.py: -------------------------------------------------------------------------------- 1 | """Unit tests: upgrade""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("condition", ["override", "equal", "existing_repo"]) 7 | def test_upgrade_errors(tmpdir, runner, yadm, condition): 8 | """Test upgrade() error conditions""" 9 | 10 | home = tmpdir.mkdir("home") 11 | yadm_dir = home.join(".config/yadm") 12 | yadm_data = home.join(".local/share/yadm") 13 | override = "" 14 | if condition == "override": 15 | override = "override" 16 | if condition == "equal": 17 | yadm_data = yadm_dir 18 | if condition == "existing_repo": 19 | yadm_dir.ensure_dir("repo.git") 20 | yadm_data.ensure_dir("repo.git") 21 | 22 | script = f""" 23 | YADM_TEST=1 source {yadm} 24 | YADM_DIR="{yadm_dir}" 25 | YADM_DATA="{yadm_data}" 26 | YADM_REPO="{yadm_data}/repo.git" 27 | YADM_LEGACY_ARCHIVE="files.gpg" 28 | YADM_OVERRIDE_REPO="{override}" 29 | upgrade 30 | """ 31 | run = runner(command=["bash"], inp=script) 32 | assert run.failure 33 | assert "Unable to upgrade" in run.err 34 | if condition in ["override", "equal"]: 35 | assert "Paths have been overridden" in run.err 36 | elif condition == "existing_repo": 37 | assert "already exists" in run.err 38 | 39 | 40 | @pytest.mark.parametrize("condition", ["no-paths", "untracked", "tracked", "submodules"]) 41 | def test_upgrade(tmpdir, runner, yadm, condition): 42 | """Test upgrade() 43 | 44 | When testing the condition of git-tracked data, "echo" will be used as a 45 | mock for git. echo will return true, simulating a positive result from "git 46 | ls-files". Also echo will report the parameters for "git mv". 47 | """ 48 | legacy_paths = ("config", "encrypt", "bootstrap", "hooks/pre_cmd") 49 | home = tmpdir.mkdir("home") 50 | yadm_dir = home.join(".config/yadm") 51 | yadm_data = home.join(".local/share/yadm") 52 | yadm_legacy = home.join(".yadm") 53 | 54 | if condition != "no-paths": 55 | yadm_dir.join("repo.git/config").write("test-repo", ensure=True) 56 | yadm_dir.join("files.gpg").write("files.gpg", ensure=True) 57 | for path in legacy_paths: 58 | yadm_legacy.join(path).write(path, ensure=True) 59 | 60 | mock_git = "" 61 | if condition != "no-paths": 62 | mock_git = f""" 63 | function git() {{ 64 | echo "$@" 65 | if [[ "$*" = *"submodule status" ]]; then 66 | {'echo " 1234567 mymodule (1.0)"' if condition == 'submodules' else ':'} 67 | fi 68 | if [[ "$*" = *ls-files* ]]; then 69 | return {1 if condition == 'untracked' else 0} 70 | fi 71 | return 0 72 | }} 73 | """ 74 | 75 | script = f""" 76 | YADM_TEST=1 source {yadm} 77 | YADM_LEGACY_DIR="{yadm_legacy}" 78 | YADM_DIR="{yadm_dir}" 79 | YADM_DATA="{yadm_data}" 80 | YADM_REPO="{yadm_data}/repo.git" 81 | YADM_ARCHIVE="{yadm_data}/archive" 82 | GIT_PROGRAM="git" 83 | {mock_git} 84 | function cd {{ echo "$@";}} 85 | upgrade 86 | """ 87 | run = runner(command=["bash"], inp=script) 88 | assert run.success 89 | assert run.err == "" 90 | if condition == "no-paths": 91 | assert "Upgrade is not necessary" in run.out 92 | else: 93 | for lpath, npath in [("repo.git", "repo.git"), ("files.gpg", "archive")]: 94 | expected = f"Moving {yadm_dir.join(lpath)} " f"to {yadm_data.join(npath)}" 95 | assert expected in run.out 96 | for path in legacy_paths: 97 | expected = f"Moving {yadm_legacy.join(path)} " f"to {yadm_dir.join(path)}" 98 | assert expected in run.out 99 | if condition == "untracked": 100 | assert "test-repo" in yadm_data.join("repo.git/config").read() 101 | assert "files.gpg" in yadm_data.join("archive").read() 102 | for path in legacy_paths: 103 | assert path in yadm_dir.join(path).read() 104 | elif condition in ["tracked", "submodules"]: 105 | expected = f'mv {yadm_dir.join("files.gpg")} ' f'{yadm_data.join("archive")}' 106 | assert expected in run.out 107 | assert "files tracked by yadm have been renamed" in run.out 108 | if condition == "submodules": 109 | assert "submodule deinit -- mymodule" in run.out 110 | assert "submodule update --init --recursive -- mymodule" in run.out 111 | else: 112 | assert "submodule deinit" not in run.out 113 | assert "submodule update --init --recursive" not in run.out 114 | -------------------------------------------------------------------------------- /test/test_unit_x_program.py: -------------------------------------------------------------------------------- 1 | """Unit tests: yadm.[git,gpg]-program""" 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "executable, success, value, match", 10 | [ 11 | (None, True, "program", None), 12 | ("cat", True, "cat", None), 13 | ("badprogram", False, None, "badprogram"), 14 | ], 15 | ids=[ 16 | "executable missing", 17 | "valid alternative", 18 | "invalid alternative", 19 | ], 20 | ) 21 | @pytest.mark.parametrize("program", ["git", "gpg"]) 22 | def test_x_program(runner, yadm_cmd, paths, program, executable, success, value, match): 23 | """Set yadm.X-program, and test result of require_X""" 24 | 25 | # set configuration 26 | if executable: 27 | os.system(" ".join(yadm_cmd("config", f"yadm.{program}-program", executable))) 28 | 29 | # test require_[git,gpg] 30 | script = f""" 31 | YADM_TEST=1 source {paths.pgm} 32 | YADM_OVERRIDE_CONFIG="{paths.config}" 33 | configure_paths 34 | require_{program} 35 | echo ${program.upper()}_PROGRAM 36 | """ 37 | run = runner(command=["bash"], inp=script) 38 | assert run.success == success 39 | 40 | # [GIT,GPG]_PROGRAM set correctly 41 | if value == "program": 42 | assert run.out.rstrip() == program 43 | elif value: 44 | assert run.out.rstrip() == value 45 | 46 | # error reported about bad config 47 | if match: 48 | assert match in run.err 49 | else: 50 | assert run.err == "" 51 | -------------------------------------------------------------------------------- /test/test_upgrade.py: -------------------------------------------------------------------------------- 1 | """Test upgrade""" 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "versions", 10 | [ 11 | ("1.12.0", "2.5.0"), 12 | ("1.12.0",), 13 | ("2.5.0",), 14 | ], 15 | ids=[ 16 | "1.12.0 -> 2.5.0 -> latest", 17 | "1.12.0 -> latest", 18 | "2.5.0 -> latest", 19 | ], 20 | ) 21 | @pytest.mark.parametrize("submodule", [False, True], ids=["no submodule", "with submodules"]) 22 | def test_upgrade(tmpdir, runner, paths, versions, submodule): 23 | """Upgrade tests""" 24 | # pylint: disable=too-many-statements 25 | home = tmpdir.mkdir("HOME") 26 | env = {"HOME": str(home)} 27 | runner(["git", "config", "--global", "init.defaultBranch", "master"], env=env) 28 | runner(["git", "config", "--global", "protocol.file.allow", "always"], env=env) 29 | runner(["git", "config", "--global", "user.email", "test@yadm.io"], env=env) 30 | runner(["git", "config", "--global", "user.name", "Yadm Test"], env=env) 31 | 32 | if submodule: 33 | ext_repo = tmpdir.mkdir("ext_repo") 34 | ext_repo.join("afile").write("some data") 35 | 36 | for cmd in (("init",), ("add", "afile"), ("commit", "-m", "test")): 37 | run = runner(["git", "-C", str(ext_repo), *cmd]) 38 | assert run.success 39 | 40 | os.environ.pop("XDG_CONFIG_HOME", None) 41 | os.environ.pop("XDG_DATA_HOME", None) 42 | 43 | def run_version(version, *args, check_stderr=True): 44 | yadm = f"yadm-{version}" if version else paths.pgm 45 | run = runner([yadm, *args], shell=True, cwd=str(home), env=env) 46 | assert run.success 47 | if check_stderr: 48 | assert run.err == "" 49 | return run 50 | 51 | # Initialize the repo with the first version 52 | first = versions[0] 53 | run_version(first, "init") 54 | 55 | home.join("file").write("some data") 56 | run_version(first, "add", "file") 57 | run_version(first, "commit", "-m", '"First commit"') 58 | 59 | if submodule: 60 | # When upgrading via 2.5.0 we can't have a submodule that's been added 61 | # after being cloned as 2.5.0 fails the upgrade in that case. 62 | can_upgrade_cloned_submodule = "2.5.0" not in versions[1:] 63 | if can_upgrade_cloned_submodule: 64 | # Check out a repo and then add it as a submodule 65 | run = runner(["git", "-C", str(home), "clone", str(ext_repo), "b"]) 66 | assert run.success 67 | run_version(first, "submodule", "add", str(ext_repo), "b") 68 | 69 | # Add submodule without first checking it out 70 | run_version(first, "submodule", "add", str(ext_repo), "a", check_stderr=False) 71 | run_version(first, "submodule", "add", str(ext_repo), "c", check_stderr=False) 72 | 73 | run_version(first, "commit", "-m", '"Add submodules"') 74 | 75 | for path in (".yadm", ".config/yadm"): 76 | yadm_dir = home.join(path) 77 | if yadm_dir.exists(): 78 | break 79 | 80 | yadm_dir.join("bootstrap").write("init stuff") 81 | run_version(first, "add", yadm_dir.join("bootstrap")) 82 | run_version(first, "commit", "-m", "bootstrap") 83 | 84 | yadm_dir.join("encrypt").write("secret") 85 | 86 | hooks_dir = yadm_dir.mkdir("hooks") 87 | hooks_dir.join("pre_status").write("status") 88 | hooks_dir.join("post_commit").write("commit") 89 | 90 | run_version(first, "config", "local.class", "test") 91 | run_version(first, "config", "foo.bar", "true") 92 | 93 | # Run upgrade with intermediate versions and latest 94 | latest = None 95 | for version in versions[1:] + (latest,): 96 | run = run_version(version, "upgrade", check_stderr=not submodule) 97 | if submodule: 98 | lines = run.err.splitlines() 99 | if can_upgrade_cloned_submodule: 100 | assert "Migrating git directory of" in lines[0] 101 | assert str(home.join("b/.git")) in lines[1] 102 | assert str(yadm_dir.join("repo.git/modules/b")) in lines[2] 103 | del lines[:3] 104 | for line in lines: 105 | assert line.startswith("Submodule") 106 | assert "registered for path" in line 107 | 108 | # Verify result for the final upgrade 109 | run_version(latest, "status") 110 | 111 | run = run_version(latest, "show", "HEAD:file") 112 | assert run.out == "some data" 113 | 114 | if submodule: 115 | if can_upgrade_cloned_submodule: 116 | assert home.join("b/afile").read() == "some data" 117 | assert home.join("a/afile").read() == "some data" 118 | assert home.join("c/afile").read() == "some data" 119 | 120 | yadm_dir = home.join(".config/yadm") 121 | 122 | assert yadm_dir.join("bootstrap").read() == "init stuff" 123 | assert yadm_dir.join("encrypt").read() == "secret" 124 | 125 | hooks_dir = yadm_dir.join("hooks") 126 | assert hooks_dir.join("pre_status").read() == "status" 127 | assert hooks_dir.join("post_commit").read() == "commit" 128 | 129 | run = run_version(latest, "config", "local.class") 130 | assert run.out.rstrip() == "test" 131 | 132 | run = run_version(latest, "config", "foo.bar") 133 | assert run.out.rstrip() == "true" 134 | -------------------------------------------------------------------------------- /test/test_version.py: -------------------------------------------------------------------------------- 1 | """Test version""" 2 | 3 | import re 4 | 5 | import pytest 6 | 7 | 8 | @pytest.fixture(scope="module") 9 | def expected_version(yadm): 10 | """ 11 | Expected semantic version number. This is taken directly out of yadm, 12 | searching for the VERSION= string. 13 | """ 14 | with open(yadm, encoding="utf-8") as source_file: 15 | yadm_version = re.findall(r"VERSION=([^\n]+)", source_file.read()) 16 | if yadm_version: 17 | return yadm_version[0] 18 | pytest.fail(f"version not found in {yadm}") 19 | return "not found" 20 | 21 | 22 | def test_semantic_version(expected_version): 23 | """Version is semantic""" 24 | # semantic version conforms to MAJOR.MINOR.PATCH 25 | assert re.search(r"^\d+\.\d+\.\d+$", expected_version), "does not conform to MAJOR.MINOR.PATCH" 26 | 27 | 28 | @pytest.mark.parametrize("cmd", ["--version", "version"]) 29 | def test_reported_version(runner, yadm_cmd, cmd, expected_version): 30 | """Report correct version and bash/git versions""" 31 | run = runner(command=yadm_cmd(cmd)) 32 | assert run.success 33 | assert run.err == "" 34 | assert "bash version" in run.out 35 | assert "git version" in run.out 36 | assert run.out.endswith(f"\nyadm version {expected_version}\n") 37 | -------------------------------------------------------------------------------- /test/utils.py: -------------------------------------------------------------------------------- 1 | """Testing Utilities 2 | 3 | This module holds values/functions common to multiple tests. 4 | """ 5 | 6 | import os 7 | import re 8 | 9 | ALT_FILE1 = "test_alt" 10 | ALT_FILE2 = "test alt/test alt" 11 | ALT_DIR = "test alt/test alt dir" 12 | 13 | # Directory based alternates must have a tracked contained file. 14 | # This will be the test contained file name 15 | CONTAINED = "contained_dir/contained_file" 16 | 17 | # These variables are used for making include files which will be processed 18 | # within jinja templates 19 | INCLUDE_FILE = "inc_file" 20 | INCLUDE_DIRS = ["", "test alt"] 21 | INCLUDE_CONTENT = "8780846c02e34c930d0afd127906668f" 22 | 23 | 24 | def set_local(paths, variable, value, add=False): 25 | """Set local override""" 26 | add = "--add" if add else "" 27 | os.system(f"GIT_DIR={str(paths.repo)} " f'git config --local {add} "local.{variable}" "{value}"') 28 | 29 | 30 | def create_alt_files( 31 | paths, 32 | suffix, 33 | preserve=False, 34 | tracked=True, 35 | encrypt=False, 36 | exclude=False, 37 | content=None, 38 | includefile=False, 39 | yadm_alt=False, 40 | yadm_dir=None, 41 | ): 42 | """Create new files, and add to the repo 43 | 44 | This is used for testing alternate files. In each case, a suffix is 45 | appended to two standard file paths. Particulars of the file creation and 46 | repo handling are dependent upon the function arguments. 47 | """ 48 | 49 | basepath = yadm_dir.join("alt") if yadm_alt else paths.work 50 | 51 | if not preserve: 52 | for remove_path in (ALT_FILE1, ALT_FILE2, ALT_DIR): 53 | if basepath.join(remove_path).exists(): 54 | basepath.join(remove_path).remove(rec=1, ignore_errors=True) 55 | assert not basepath.join(remove_path).exists() 56 | 57 | new_file1 = basepath.join(ALT_FILE1 + suffix) 58 | new_file1.write(ALT_FILE1 + suffix, ensure=True) 59 | new_file2 = basepath.join(ALT_FILE2 + suffix) 60 | new_file2.write(ALT_FILE2 + suffix, ensure=True) 61 | new_dir = basepath.join(ALT_DIR + suffix).join(CONTAINED) 62 | new_dir.write(ALT_DIR + suffix, ensure=True) 63 | 64 | test_paths = [new_file1, new_file2, new_dir] 65 | test_names = [ALT_FILE1, ALT_FILE2, ALT_DIR] 66 | 67 | for test_path in test_paths: 68 | if content: 69 | test_path.write("\n" + content, mode="a", ensure=True) 70 | assert test_path.exists() 71 | 72 | _create_includefiles(includefile, test_paths, basepath) 73 | _create_tracked(tracked, test_paths, paths) 74 | 75 | prefix = ".config/yadm/alt/" if yadm_alt else "" 76 | _create_encrypt(encrypt, test_names, suffix, paths, exclude, prefix) 77 | 78 | 79 | def parse_alt_output(output, linked=True): 80 | """Parse output of 'alt', and return list of linked files""" 81 | regex = r"Creating (.+) from template (.+)$" 82 | if linked: 83 | regex = r"(?:Copy|Link)ing (.+) to (.+)$" 84 | parsed_list = {} 85 | for line in output.splitlines(): 86 | match = re.match(regex, line) 87 | if match: 88 | if linked: 89 | parsed_list[match.group(2)] = match.group(1) 90 | else: 91 | parsed_list[match.group(1)] = match.group(2) 92 | return parsed_list.values() 93 | 94 | 95 | def _create_includefiles(includefile, test_paths, basepath): 96 | if includefile: 97 | for dpath in INCLUDE_DIRS: 98 | incfile = basepath.join(dpath + "/" + INCLUDE_FILE) 99 | incfile.write(INCLUDE_CONTENT, ensure=True) 100 | test_paths += [incfile] 101 | 102 | 103 | def _create_tracked(tracked, test_paths, paths): 104 | if tracked: 105 | for track_path in test_paths: 106 | os.system(f'GIT_DIR={str(paths.repo)} git add "{track_path}"') 107 | os.system(f'GIT_DIR={str(paths.repo)} git commit -m "Add test files"') 108 | 109 | 110 | def _create_encrypt(encrypt, test_names, suffix, paths, exclude, prefix): 111 | if encrypt: 112 | for encrypt_name in test_names: 113 | paths.encrypt.write(f"{prefix + encrypt_name + suffix}\n", mode="a") 114 | if exclude: 115 | paths.encrypt.write(f"!{prefix + encrypt_name + suffix}\n", mode="a") 116 | -------------------------------------------------------------------------------- /yadm.spec: -------------------------------------------------------------------------------- 1 | %{!?_pkgdocdir: %global _pkgdocdir %{_docdir}/%{name}-%{version}} 2 | Name: yadm 3 | Summary: Yet Another Dotfiles Manager 4 | Version: 3.5.0 5 | Group: Development/Tools 6 | Release: 1%{?dist} 7 | URL: https://yadm.io 8 | License: GPL-3.0-only 9 | Requires: bash 10 | Requires: git 11 | 12 | Source: %{name}.tar.gz 13 | BuildRoot: %{_tmppath}/%{name}-%{version}-build 14 | BuildArch: noarch 15 | 16 | %description 17 | yadm is a tool for managing a collection of files across multiple computers, 18 | using a shared Git repository. In addition, yadm provides a feature to select 19 | alternate versions of files based on the operation system or host name. Lastly, 20 | yadm supplies the ability to manage a subset of secure files, which are 21 | encrypted before they are included in the repository. 22 | 23 | %prep 24 | %setup -c 25 | 26 | %build 27 | 28 | %install 29 | 30 | # this is done to allow paths other than yadm-x.x.x (for example, when building 31 | # from branches instead of release tags) 32 | test -f yadm || cd *yadm-* 33 | 34 | %{__mkdir} -p %{buildroot}%{_bindir} 35 | %{__cp} yadm %{buildroot}%{_bindir} 36 | 37 | %{__mkdir} -p %{buildroot}%{_mandir}/man1 38 | %{__cp} yadm.1 %{buildroot}%{_mandir}/man1 39 | 40 | %{__mkdir} -p %{buildroot}%{_pkgdocdir} 41 | %{__cp} README.md %{buildroot}%{_pkgdocdir}/README 42 | %{__cp} CHANGES CONTRIBUTORS LICENSE %{buildroot}%{_pkgdocdir} 43 | %{__cp} -r completion contrib %{buildroot}%{_pkgdocdir} 44 | 45 | %files 46 | %attr(755,root,root) %{_bindir}/yadm 47 | %attr(644,root,root) %{_mandir}/man1/* 48 | %doc %{_pkgdocdir} 49 | --------------------------------------------------------------------------------