diff --git a/test/conftest.py b/test/conftest.py index 805ee92..a037223 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -101,6 +101,7 @@ def supported_configs(): 'local.user', 'yadm.alt-copy', 'yadm.auto-alt', + 'yadm.auto-exclude', 'yadm.auto-perms', 'yadm.auto-private-dirs', 'yadm.git-program', diff --git a/test/test_encryption.py b/test/test_encryption.py index f107ad5..3d10786 100644 --- a/test/test_encryption.py +++ b/test/test_encryption.py @@ -372,6 +372,29 @@ def test_offer_to_add(runner, yadm_y, paths, encrypt_targets, untracked): assert f'AM {worktree_archive.basename}' in run.out +def test_encrypt_added_to_exclude(runner, yadm_y, paths): + """Confirm that .config/yadm/encrypt is added to exclude""" + + expect = [ + ('passphrase:', PASSPHRASE), + ('passphrase:', PASSPHRASE), + ] + + exclude_file = paths.repo.join('info/exclude') + paths.encrypt.write('test-encrypt-data\n') + exclude_file.write('original-data', ensure=True) + + run = runner( + yadm_y('encrypt'), + expect=expect, + ) + + assert 'test-encrypt-data' in paths.repo.join('info/exclude').read() + assert 'original-data' in paths.repo.join('info/exclude').read() + assert run.success + assert run.err == '' + + def encrypted_data_valid(runner, encrypted, expected): """Verify encrypted data matches expectations""" run = runner([ diff --git a/test/test_introspect.py b/test/test_introspect.py index 9026e98..fcadf14 100644 --- a/test/test_introspect.py +++ b/test/test_introspect.py @@ -27,7 +27,7 @@ def test_introspect_category( expected = [] if name == 'commands': expected = supported_commands - elif name == 'config': + elif name == 'configs': expected = supported_configs elif name == 'switches': expected = supported_switches diff --git a/test/test_unit_exclude_encrypted.py b/test/test_unit_exclude_encrypted.py new file mode 100644 index 0000000..9d9a074 --- /dev/null +++ b/test/test_unit_exclude_encrypted.py @@ -0,0 +1,66 @@ +"""Unit tests: exclude_encrypted""" +import pytest + + +@pytest.mark.parametrize( + 'exclude', ['missing', 'outdated', 'up-to-date']) +@pytest.mark.parametrize( + 'encrypt_exists', [True, False], ids=['encrypt', 'no-encrypt']) +@pytest.mark.parametrize( + 'auto_exclude', [True, False], ids=['enabled', 'disabled']) +def test_exclude_encrypted( + runner, tmpdir, yadm, encrypt_exists, auto_exclude, exclude): + """Test exclude_encrypted()""" + + header = ( + "# yadm-auto-excludes\n" + "# This section is managed by yadm.\n" + "# Any edits below will be lost.\n" + ) + + config_function = 'function config() { echo "false";}' + if auto_exclude: + config_function = 'function config() { return; }' + + encrypt_file = tmpdir.join('encrypt_file') + repo_dir = tmpdir.join('repodir') + exclude_file = repo_dir.join('info/exclude') + + if encrypt_exists: + encrypt_file.write('test-encrypt-data\n', ensure=True) + if exclude == 'outdated': + exclude_file.write( + f'original-exclude\n{header}outdated\n', ensure=True) + elif exclude == 'up-to-date': + exclude_file.write( + f'original-exclude\n{header}test-encrypt-data\n', ensure=True) + + script = f""" + YADM_TEST=1 source {yadm} + {config_function} + DEBUG=1 + YADM_ENCRYPT="{encrypt_file}" + YADM_REPO="{repo_dir}" + exclude_encrypted + """ + run = runner(command=['bash'], inp=script) + assert run.success + assert run.err == '' + + if auto_exclude: + if encrypt_exists: + assert exclude_file.exists() + if exclude == 'missing': + assert exclude_file.read() == f'{header}test-encrypt-data\n' + else: + assert exclude_file.read() == ( + 'original-exclude\n' + f'{header}test-encrypt-data\n') + if exclude != 'up-to-date': + assert f'Updating {exclude_file}' in run.out + else: + assert run.out == '' + else: + assert run.out == '' + else: + assert run.out == '' diff --git a/yadm b/yadm index eb55852..7f70b77 100755 --- a/yadm +++ b/yadm @@ -804,6 +804,7 @@ function encrypt() { require_gpg require_encrypt + exclude_encrypted parse_encrypt cd_work "Encryption" || return @@ -986,6 +987,7 @@ local.os local.user yadm.alt-copy yadm.auto-alt +yadm.auto-exclude yadm.auto-perms yadm.auto-private-dirs yadm.git-program @@ -1069,6 +1071,55 @@ function version() { # ****** Utility Functions ****** +function exclude_encrypted() { + + auto_exclude=$(config --bool yadm.auto-exclude) + [ "$auto_exclude" == "false" ] && return 0 + + exclude_path="${YADM_REPO}/info/exclude" + newline=$'\n' + exclude_flag="# yadm-auto-excludes" + exclude_header="${exclude_flag}${newline}" + exclude_header="${exclude_header}# This section is managed by yadm." + exclude_header="${exclude_header}${newline}" + exclude_header="${exclude_header}# Any edits below will be lost." + exclude_header="${exclude_header}${newline}" + + # do nothing if there is no YADM_ENCRYPT + [ -e "$YADM_ENCRYPT" ] || return 0 + + # read encrypt + encrypt_data="" + while IFS='' read -r line || [ -n "$line" ]; do + encrypt_data="${encrypt_data}${line}${newline}" + done < "$YADM_ENCRYPT" + + # read info/exclude + unmanaged="" + managed="" + if [ -e "$exclude_path" ]; then + flag_seen=0 + while IFS='' read -r line || [ -n "$line" ]; do + [ "$line" = "$exclude_flag" ] && flag_seen=1 + if [ "$flag_seen" -eq 0 ]; then + unmanaged="${unmanaged}${line}${newline}" + else + managed="${managed}${line}${newline}" + fi + done < "$exclude_path" + fi + + if [ "${exclude_header}${encrypt_data}" != "$managed" ]; then + basedir=${exclude_path%/*} + [ -e "$basedir" ] || mkdir -p "$basedir" # assert path + debug "Updating ${exclude_path}" + printf "%s" "${unmanaged}${exclude_header}${encrypt_data}" > "$exclude_path" + fi + + return 0 + +} + function is_valid_branch_name() { # Git branches do not allow: # * path component that begins with "." diff --git a/yadm.1 b/yadm.1 index aff25f3..72032bc 100644 --- a/yadm.1 +++ b/yadm.1 @@ -347,6 +347,11 @@ Disable the automatic linking described in the section ALTERNATES. If disabled, you may still run "yadm alt" manually to create the alternate links. This feature is enabled by default. .TP +.B yadm.auto-exclude +Disable the automatic exclusion of patterns defined in +.IR $HOME/.config/yadm/encrypt . +This feature is enabled by default. +.TP .B yadm.auto-perms Disable the automatic permission changes described in the section PERMISSIONS. If disabled, you may still run @@ -674,6 +679,18 @@ configuration. It is recommended that you use a private repository when keeping confidential files, even though they are encrypted. +Patterns found in +.I $HOME/.config/yadm/encrypt +are automatically added to the repository's +.I info/exclude +file every time +.B yadm encrypt +is run. +This is to prevent accidentally committing sensitive data to the repository. +This can be disabled using the +.I yadm.auto-exclude +configuration. + .SH PERMISSIONS When files are checked out of a Git repository, their initial permissions are