Skip to content

Commit bf2b0e5

Browse files
authored
Merge pull request #76 from elasticdog/69-fix-merge-of-encrypted-files-with-conflicts
Fix `transcrypt`'s handling of merges where encrypted files have conflicting changes, a situation which would lead to Git producing "merged" files with conflict markers around partially- or fully-encrypted content that cannot be sensibly merged by a person. See issue #69 and a bunch of related issues. The root problem is that git does not run the `smudge`/`textconv` filter on all BASE, LOCAL, REMOTE conflicting version files before attempting a three-way merge. This change adds: - a merge driver script to pre-decrypt conflicting BASE, LOCAL, and REMOTE file versions then run git's internal `merge-file` command to merge the decrypted versions - git repo settings to configure the merge driver - recommendation to add the extra `merge=crypt` setting to *.gitattribute* definitions - tests of merge functionality to prove that non-conflicting and conflicting merges work. Also included are minor listing and formatting fixes from applying the recommended tools to do this clean-up, and documentation for how to run these tools in *README.md* The bulk of this work is originally from https://github.com/ixc/transcrypt/commits/fix-merge-with-conflicts
2 parents f3a9914 + 44f1c0e commit bf2b0e5

File tree

6 files changed

+152
-11
lines changed

6 files changed

+152
-11
lines changed

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ insert_final_newline = true
77
trim_trailing_whitespace = true
88

99
[*.md]
10-
indent_size = 4
10+
indent_size = 2
1111
indent_style = space
1212
trim_trailing_whitespace = false
1313

README.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,14 @@ using the command line options. Run `transcrypt --help` for more details.
9292
### Designate a File to be Encrypted
9393

9494
Once a repository has been configured with transcrypt, you can designate for
95-
files to be encrypted by applying the "crypt" filter and diff to a
95+
files to be encrypted by applying the "crypt" filter, diff, and merge to a
9696
[pattern](https://www.kernel.org/pub/software/scm/git/docs/gitignore.html#_pattern_format)
9797
in the top-level _[.gitattributes](http://git-scm.com/docs/gitattributes)_
9898
config. If that pattern matches a file in your repository, the file will be
9999
transparently encrypted once you stage and commit it:
100100

101101
$ cd <path-to-your-repo>/
102-
$ echo 'sensitive_file filter=crypt diff=crypt' >> .gitattributes
102+
$ echo 'sensitive_file filter=crypt diff=crypt merge=crypt' >> .gitattributes
103103
$ git add .gitattributes sensitive_file
104104
$ git commit -m 'Add encrypted version of a sensitive file'
105105

@@ -297,11 +297,22 @@ Copyright &copy; 2014-2020, [Aaron Bull Schaefer](mailto:[email protected]).
297297

298298
## Contributing
299299

300+
### Linting and formatting
301+
302+
Please use:
303+
304+
- the [shellcheck](https://www.shellcheck.net) tool to check for subtle bash
305+
scripting errors in the _transcrypt_ file, and apply the recommendations when
306+
possible. E.g: `shellcheck transcrypt`
307+
- the [shfmt](https://github.com/mvdan/sh) tool to apply consistent formatting
308+
to the _transcrypt_ file, e.g: `shfmt -w transcrypt`
309+
- the [Prettier](https://prettier.io) tool to apply consistent formatting to the
310+
_README.md_ file, e.g: `prettier --write README.md`
311+
300312
### Tests
301313

302314
Tests are written using [bats-core](https://github.com/bats-core/bats-core)
303-
version of "Bash Automated Testing System" and stored in the *tests/*
304-
directory.
315+
version of "Bash Automated Testing System" and stored in the _tests/_ directory.
305316

306317
To run the tests:
307318

@@ -311,6 +322,15 @@ To run the tests:
311322

312323
## Changes
313324

325+
Fixes:
326+
327+
- Fix handling of branch merges with conflicts in encrypted files, which would
328+
previously leave the user to manually merge files with a mix of encrypted and
329+
unencrypted content.
330+
331+
To apply this fix in projects that already use transcrypt: uninstall and
332+
re-init transcrypt, then add `merge=crypt` to the patterns in _.gitattributes_
333+
314334
Improvements:
315335

316336
- Add Git pre-commit hook to reject commit of file that should be encrypted but

tests/_test_helper.bash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function encrypt_named_file {
4343
if [ "$content" ]; then
4444
echo "$content" > $filename
4545
fi
46-
echo "$filename filter=crypt diff=crypt" >> .gitattributes
46+
echo "$filename filter=crypt diff=crypt merge=crypt" >> .gitattributes
4747
git add .gitattributes $filename
4848
git commit -m "Encrypt file $filename"
4949
}

tests/test_init.bats

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ SETUP_SKIP_INIT_TRANSCRYPT=1
1717
init_transcrypt
1818
[ -f .gitattributes ]
1919
run cat .gitattributes
20-
[ "${lines[0]}" = "#pattern filter=crypt diff=crypt" ]
20+
[ "${lines[0]}" = "#pattern filter=crypt diff=crypt merge=crypt" ]
2121
}
2222

2323
@test "init: creates scripts in .git/crypt/" {

tests/test_merge.bats

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env bats
2+
3+
load $BATS_TEST_DIRNAME/_test_helper.bash
4+
5+
@test "merge: branches with encrypted file - addition, no conflict" {
6+
echo "1. First step" > sensitive_file
7+
encrypt_named_file sensitive_file
8+
9+
git checkout -b branch-2
10+
echo "2. Second step" >> sensitive_file
11+
git add sensitive_file
12+
git commit -m "Add line 2"
13+
14+
git checkout -
15+
git merge branch-2
16+
17+
run cat sensitive_file
18+
[ "$status" -eq 0 ]
19+
[ "${lines[0]}" = "1. First step" ]
20+
[ "${lines[1]}" = "2. Second step" ]
21+
}
22+
23+
@test "merge: branches with encrypted file - line change, no conflict" {
24+
echo "1. First step" > sensitive_file
25+
encrypt_named_file sensitive_file
26+
27+
git checkout -b branch-2
28+
echo "1. Step the first" > sensitive_file # Cause line conflict
29+
echo "2. Second step" >> sensitive_file
30+
git add sensitive_file
31+
git commit -m "Add line 2, change line 1"
32+
33+
git checkout -
34+
git merge branch-2
35+
36+
run cat sensitive_file
37+
[ "$status" -eq 0 ]
38+
[ "${lines[0]}" = "1. Step the first" ]
39+
[ "${lines[1]}" = "2. Second step" ]
40+
}
41+
42+
@test "merge: branches with encrypted file - with conflicts" {
43+
echo "1. First step" > sensitive_file
44+
encrypt_named_file sensitive_file
45+
46+
git checkout -b branch-2
47+
echo "1. Step the first" > sensitive_file # Cause line conflict
48+
echo "2. Second step" >> sensitive_file
49+
git add sensitive_file
50+
git commit -m "Add line 2, change line 1"
51+
52+
git checkout -
53+
echo "a. First step" > sensitive_file
54+
git add sensitive_file
55+
git commit -m "Change line 1 in original branch to set up conflict"
56+
57+
run git merge branch-2
58+
[ "$status" -ne 0 ]
59+
[ "${lines[1]}" = "CONFLICT (content): Merge conflict in sensitive_file" ]
60+
61+
run cat sensitive_file
62+
[ "$status" -eq 0 ]
63+
[ "${lines[0]}" = "<<<<<<< master" ]
64+
[ "${lines[1]}" = "a. First step" ]
65+
[ "${lines[2]}" = "=======" ]
66+
[ "${lines[3]}" = "1. Step the first" ]
67+
[ "${lines[4]}" = "2. Second step" ]
68+
[ "${lines[5]}" = ">>>>>>> branch-2" ]
69+
}

transcrypt

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -322,8 +322,52 @@ save_helper_scripts() {
322322
fi
323323
EOF
324324

325+
cat <<-'EOF' >"${GIT_DIR}/crypt/merge"
326+
#!/usr/bin/env bash
327+
328+
# Look up name of local branch/ref to which changes are being merged
329+
OURS_LABEL=$(git rev-parse --abbrev-ref HEAD)
330+
331+
# Look up name of the incoming "theirs" branch/ref being merged in.
332+
# TODO There must be a better way of doing this than relying on this reflog
333+
# action environment variable, but I don't know what it is
334+
if [[ "$GIT_REFLOG_ACTION" = "merge "* ]]; then
335+
THEIRS_LABEL=$(echo $GIT_REFLOG_ACTION | awk '{print $2}')
336+
fi
337+
if [[ ! "$THEIRS_LABEL" ]]; then
338+
THEIRS_LABEL="theirs"
339+
fi
340+
341+
# Decrypt BASE, LOCAL, and REMOTE versions of file being merged
342+
echo "$(cat $1 | ./.git/crypt/smudge)" > $1
343+
echo "$(cat $2 | ./.git/crypt/smudge)" > $2
344+
echo "$(cat $3 | ./.git/crypt/smudge)" > $3
345+
346+
# Merge the decrypted files to the working copy named by $5
347+
# We must redirect stdout to $5 here instead of letting merge-file write to
348+
# $2 as it would by default, because we need $5 to contain the final result
349+
# content so a later crypt `clean` generates the correct hash salt value
350+
git merge-file --stdout --marker-size=$4 -L $OURS_LABEL -L base -L $THEIRS_LABEL $2 $1 $3 > $5
351+
352+
if [[ "$?" == "0" ]]; then
353+
# If the merge was successful (no conflicts) re-encrypt the merged working
354+
# copy file to the incoming "local" temp file $2 which git will then
355+
# update in the index during the "Auto-merging" step.
356+
# Git needs the cleaned copy to avoid triggering the error:
357+
# error: add_cacheinfo failed to refresh for path 'FILE'; merge aborting.
358+
echo "$(cat $5 | ./.git/crypt/clean $5)" > $2
359+
exit 0
360+
else
361+
# If the merge was not successful, copy the merged working copy file to the
362+
# "local" temp file $2 which git will then re-copy back to the working copy
363+
# so the user can fix it manually
364+
cp $5 $2
365+
exit 1
366+
fi
367+
EOF
368+
325369
# make scripts executable
326-
for script in {clean,smudge,textconv}; do
370+
for script in {clean,smudge,textconv,merge}; do
327371
chmod 0755 "${GIT_DIR}/crypt/${script}"
328372
done
329373
}
@@ -392,18 +436,23 @@ save_configuration() {
392436
git config filter.crypt.smudge '"$(git rev-parse --git-common-dir)"/crypt/smudge'
393437
# shellcheck disable=SC2016
394438
git config diff.crypt.textconv '"$(git rev-parse --git-common-dir)"/crypt/textconv'
439+
# shellcheck disable=SC2016
440+
git config merge.crypt.driver '"$(git rev-parse --git-common-dir)"/crypt/merge %O %A %B %L %P'
395441
else
396442
# shellcheck disable=SC2016
397443
git config filter.crypt.clean '"$(git rev-parse --git-dir)"/crypt/clean %f'
398444
# shellcheck disable=SC2016
399445
git config filter.crypt.smudge '"$(git rev-parse --git-dir)"/crypt/smudge'
400446
# shellcheck disable=SC2016
401447
git config diff.crypt.textconv '"$(git rev-parse --git-dir)"/crypt/textconv'
448+
# shellcheck disable=SC2016
449+
git config merge.crypt.driver '"$(git rev-parse --git-dir)"/crypt/merge %O %A %B %L %P'
402450
fi
403451
git config filter.crypt.required 'true'
404452
git config diff.crypt.cachetextconv 'true'
405453
git config diff.crypt.binary 'true'
406454
git config merge.renormalize 'true'
455+
git config merge.crypt.name 'Merge transcrypt secret files'
407456

408457
# add a git alias for listing encrypted files
409458
git config alias.ls-crypt "!git ls-files | git check-attr --stdin filter | awk 'BEGIN { FS = \":\" }; /crypt$/{ print \$1 }'"
@@ -433,6 +482,7 @@ clean_gitconfig() {
433482
git config --remove-section transcrypt 2>/dev/null || true
434483
git config --remove-section filter.crypt 2>/dev/null || true
435484
git config --remove-section diff.crypt 2>/dev/null || true
485+
git config --remove-section merge.crypt 2>/dev/null || true
436486
git config --unset merge.renormalize
437487

438488
# remove the merge section if it's now empty
@@ -512,7 +562,7 @@ uninstall_transcrypt() {
512562
clean_gitconfig
513563

514564
# remove helper scripts
515-
for script in {clean,smudge,textconv}; do
565+
for script in {clean,smudge,textconv,merge}; do
516566
[[ ! -f "${GIT_DIR}/crypt/${script}" ]] || rm "${GIT_DIR}/crypt/${script}"
517567
done
518568
[[ ! -d "${GIT_DIR}/crypt" ]] || rmdir "${GIT_DIR}/crypt"
@@ -554,9 +604,11 @@ uninstall_transcrypt() {
554604
case $OSTYPE in
555605
darwin*)
556606
/usr/bin/sed -i '' '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
607+
/usr/bin/sed -i '' '/filter=crypt diff=crypt merge=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
557608
;;
558609
linux*)
559610
sed -i '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
611+
sed -i '/filter=crypt diff=crypt merge=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
560612
;;
561613
esac
562614

@@ -745,7 +797,7 @@ help() {
745797
a file in your repository, the file will be transparently encrypted
746798
once you stage and commit it:
747799
748-
$ echo 'sensitive_file filter=crypt diff=crypt' >> .gitattributes
800+
$ echo 'sensitive_file filter=crypt diff=crypt merge=crypt' >> .gitattributes
749801
$ git add .gitattributes sensitive_file
750802
$ git commit -m 'Add encrypted version of a sensitive file'
751803
@@ -942,7 +994,7 @@ fi
942994
# ensure the git attributes file exists
943995
if [[ ! -f $GIT_ATTRIBUTES ]]; then
944996
mkdir -p "${GIT_ATTRIBUTES%/*}"
945-
printf '#pattern filter=crypt diff=crypt\n' >"$GIT_ATTRIBUTES"
997+
printf '#pattern filter=crypt diff=crypt merge=crypt\n' >"$GIT_ATTRIBUTES"
946998
fi
947999

9481000
printf 'The repository has been successfully configured by transcrypt.\n'

0 commit comments

Comments
 (0)