Skip to content

Commit 3d5a601

Browse files
author
Dan Ellis
committed
Add atk-git-diff.
1 parent 2b19d5e commit 3d5a601

File tree

9 files changed

+298
-5
lines changed

9 files changed

+298
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
*.sw?
22
*.egg-info*
33
*.pyc
4+
/dist/

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,21 @@ It's important that the vault always be opened and closed from the
9494
base directory of your ansible project. Newer versions may attempt
9595
to detect and force this by default.
9696

97+
### atk-git-diff ###
98+
99+
Doing a `git diff` on encrypted files produces some pretty useless output.
100+
`atk-git-diff` will detect changes via `git diff` unencrypt the before and
101+
after and then show the difference.
102+
103+
This:
104+
105+
![Encrypted git diff output](https://github.com/dellis23/ansible-toolkit/blob/master/img/git-diff-encrypted.png)
106+
107+
Becomes:
108+
109+
![Unencrypted git diff output](https://github.com/dellis23/ansible-toolkit/blob/master/img/git-diff-unencrypted.png)
110+
111+
97112
Contributing
98113
------------
99114

@@ -105,6 +120,10 @@ to make it work for more environments.
105120
Changelog
106121
---------
107122

123+
### 1.3.0 ###
124+
125+
`atk-git-diff` added.
126+
108127
### 1.2.3 ###
109128

110129
Add ability to specify vault password file and inventory file on the command

ansible_toolkit/git_diff.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import difflib
2+
from itertools import islice
3+
import re
4+
import subprocess
5+
6+
from ansible.utils.vault import VaultLib
7+
8+
from utils import get_vault_password, green, red, cyan, intense
9+
10+
11+
def get_parts(git_diff_output):
12+
r = re.compile(r"^diff --git", re.MULTILINE)
13+
parts = []
14+
locations = [i.start() for i in r.finditer(git_diff_output)]
15+
for i, location in enumerate(locations):
16+
is_last_item = i + 1 == len(locations)
17+
next_location = None if is_last_item else locations[i + 1]
18+
parts.append(git_diff_output[location:next_location])
19+
return parts
20+
21+
22+
def get_old_sha(diff_part):
23+
"""
24+
Returns the SHA for the original file that was changed in a diff part.
25+
"""
26+
r = re.compile(r'index ([a-fA-F\d]*)')
27+
return r.search(diff_part).groups()[0]
28+
29+
30+
def get_old_filename(diff_part):
31+
"""
32+
Returns the filename for the original file that was changed in a diff part.
33+
"""
34+
r = re.compile(r'^--- a/(.*)', re.MULTILINE)
35+
return r.search(diff_part).groups()[0]
36+
37+
38+
def get_old_contents(sha, filename):
39+
return subprocess.check_output(['git', 'show', sha, '--', filename])
40+
41+
42+
def get_new_filename(diff_part):
43+
"""
44+
Returns the filename for the updated file in a diff part.
45+
"""
46+
r = re.compile(r'^\+\+\+ b/(.*)', re.MULTILINE)
47+
return r.search(diff_part).groups()[0]
48+
49+
50+
def get_new_contents(filename):
51+
with open(filename, 'rb') as f:
52+
return f.read()
53+
54+
55+
def get_head(diff_part):
56+
"""
57+
Returns the pre-content, non-chunk headers of a diff part.
58+
59+
E.g.
60+
61+
diff --git a/group_vars/foo b/group_vars/foo
62+
index 6b9eef7..eb9fb09 100644
63+
--- a/group_vars/foo
64+
+++ b/group_vars/foo
65+
"""
66+
return '\n'.join(diff_part.split('\n')[:4]) + '\n'
67+
68+
69+
def get_contents(diff_part):
70+
"""
71+
Returns a tuple of old content and new content.
72+
"""
73+
old_sha = get_old_sha(diff_part)
74+
old_filename = get_old_filename(diff_part)
75+
old_contents = get_old_contents(old_sha, old_filename)
76+
new_filename = get_new_filename(diff_part)
77+
new_contents = get_new_contents(new_filename)
78+
return old_contents, new_contents
79+
80+
81+
def decrypt_diff(diff_part, password_file=None):
82+
"""
83+
Diff part is a string in the format:
84+
85+
diff --git a/group_vars/foo b/group_vars/foo
86+
index c09080b..0d803bb 100644
87+
--- a/group_vars/foo
88+
+++ b/group_vars/foo
89+
@@ -1,32 +1,33 @@
90+
$ANSIBLE_VAULT;1.1;AES256
91+
-61316662363730313230626432303662316330323064373866616436623565613033396539366263
92+
-383632656663356364656531653039333965
93+
+30393563383639396563623339383936613866326332383162306532653239636166633162323236
94+
+62376161626137626133
95+
96+
Returns a tuple of decrypted old contents and decrypted new contents.
97+
"""
98+
vault = VaultLib(get_vault_password(password_file))
99+
old_contents, new_contents = get_contents(diff_part)
100+
if vault.is_encrypted(old_contents):
101+
old_contents = vault.decrypt(old_contents)
102+
if vault.is_encrypted(new_contents):
103+
new_contents = vault.decrypt(new_contents)
104+
return old_contents, new_contents
105+
106+
107+
def show_unencrypted_diff(diff_part, password_file=None):
108+
intense(get_head(diff_part).strip())
109+
old, new = decrypt_diff(diff_part, password_file)
110+
diff = difflib.unified_diff(old.split('\n'), new.split('\n'), lineterm='')
111+
# ... we'll take the git filenames from git's diff output rather than
112+
# ... difflib
113+
for line in islice(diff, 2, None):
114+
if line.startswith('-'):
115+
red(line)
116+
elif line.startswith('+'):
117+
green(line)
118+
elif line.startswith('@@'):
119+
cyan(line)
120+
else:
121+
print line
122+
123+
124+
def show_unencrypted_diffs(git_diff_output, password_file=None):
125+
parts = get_parts(git_diff_output)
126+
for part in parts:
127+
show_unencrypted_diff(part, password_file)

ansible_toolkit/tests/tests.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import tempfile
2+
import unittest
3+
4+
from ansible_toolkit import git_diff
5+
6+
7+
VAULT_PASSWORD = "foo"
8+
9+
10+
# Vaulted Files
11+
12+
OLD_FILE_1 = """$ANSIBLE_VAULT;1.1;AES256
13+
35626233663138316665643331653539633239313534376130633265353137313531656664613539
14+
6466346666333332636231356362323834616462323161610a613533363462663730366462633466
15+
36303466376432323137383661653036663338343830666264343562643833313931616165653463
16+
6637633063376664360a643964336134383034343665383730623064353338366436326135366265
17+
3038"""
18+
19+
OLD_FILE_1_DECRYPTED = """foo
20+
"""
21+
22+
NEW_FILE_1 = """$ANSIBLE_VAULT;1.1;AES256
23+
39373663313137383662393466386133396438366235323234336365623535363133353631366566
24+
3565616539353938343863633635626334636532393936390a353430663164393034616161356237
25+
34386335363662623266646637653135613337666636323834313764303766306131663334336436
26+
6633623031353931640a626366353232613963303939303130313132666437346163363535663265
27+
6535"""
28+
29+
NEW_FILE_1_DECRYPTED = """bar
30+
"""
31+
32+
33+
# Git Diff Output
34+
35+
DIFF_HEAD_1 = """diff --git a/group_vars/foo b/group_vars/foo
36+
index c09080b..0d803bb 100644
37+
--- a/group_vars/foo
38+
+++ b/group_vars/foo
39+
"""
40+
41+
SAMPLE_DIFF = DIFF_HEAD_1
42+
43+
SAMPLE_DIFF += """@@ -1,32 +1,33 @@
44+
ANSIBLE_VAULT;1.1;AES256"""
45+
46+
SAMPLE_DIFF += ''.join('\n-' + i for i in OLD_FILE_1.split('\n')[1:])
47+
SAMPLE_DIFF += ''.join('\n+' + i for i in NEW_FILE_1.split('\n')[1:])
48+
49+
# ... another section to make sure we can handle multiple parts
50+
51+
SAMPLE_DIFF += """
52+
diff --git a/group_vars/bar b/group_vars/bar
53+
index 6b9eef7..eb9fb09 100644
54+
--- a/group_vars/bar
55+
+++ b/group_vars/bar
56+
@@ -1,22 +1,23 @@
57+
$ANSIBLE_VAULT;1.1;AES256
58+
-32346330646639326335373939383634656365376531353531306238616239626265313963613561
59+
-61393637373834646566353739393762306436393234636438323434626666366136
60+
+65393432336536653066303736336632356364306533643131656461316332353138316239336137
61+
+3038396139303439356236343161396331353332326232626566"""
62+
63+
64+
class TestGitDiff(unittest.TestCase):
65+
66+
def test_get_parts(self):
67+
parts = git_diff.get_parts(SAMPLE_DIFF)
68+
self.assertEqual(len(parts), 2)
69+
70+
def test_get_old_sha(self):
71+
parts = git_diff.get_parts(SAMPLE_DIFF)
72+
old_sha = git_diff.get_old_sha(parts[0])
73+
self.assertEqual(old_sha, 'c09080b')
74+
75+
def test_get_old_filename(self):
76+
parts = git_diff.get_parts(SAMPLE_DIFF)
77+
old_filename = git_diff.get_old_filename(parts[0])
78+
self.assertEqual(old_filename, 'group_vars/foo')
79+
80+
def test_decrypt_diff(self):
81+
82+
# Monkey-patch
83+
_get_old_contents = git_diff.get_old_contents
84+
def get_old_contents(*args): # noqa
85+
return OLD_FILE_1
86+
git_diff.get_old_contents = get_old_contents
87+
_get_new_contents = git_diff.get_new_contents
88+
def get_new_contents(*args): # noqa
89+
return NEW_FILE_1
90+
git_diff.get_new_contents = get_new_contents
91+
92+
# Test decryption
93+
try:
94+
95+
# ... create temporary vault file
96+
f = tempfile.NamedTemporaryFile()
97+
f.write(VAULT_PASSWORD)
98+
f.seek(0)
99+
100+
# ... decrypt the diff
101+
parts = git_diff.get_parts(SAMPLE_DIFF)
102+
old, new = git_diff.decrypt_diff(
103+
parts[0], password_file=f.name)
104+
self.assertEqual(old, OLD_FILE_1_DECRYPTED)
105+
self.assertEqual(new, NEW_FILE_1_DECRYPTED)
106+
107+
# Restore monkey-patched functions
108+
finally:
109+
git_diff.get_old_contents = _get_old_contents
110+
git_diff.get_new_contents = _get_new_contents
111+
112+
def test_get_head(self):
113+
parts = git_diff.get_parts(SAMPLE_DIFF)
114+
head = git_diff.get_head(parts[0])
115+
self.assertEqual(head, DIFF_HEAD_1)
116+
117+
118+
if __name__ == '__main__':
119+
unittest.main()

ansible_toolkit/utils.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313

1414
# Terminal Colors
1515

16-
GREEN = '\033[92m'
17-
RED = '\033[91m'
16+
RED = '\033[31m'
17+
GREEN = '\033[32m'
18+
CYAN = '\033[36m'
19+
INTENSE = '\033[1m'
1820
ENDC = '\033[0m'
1921

2022

@@ -26,6 +28,14 @@ def red(text):
2628
print RED + text + ENDC
2729

2830

31+
def cyan(text):
32+
print CYAN + text + ENDC
33+
34+
35+
def intense(text):
36+
print INTENSE + text + ENDC
37+
38+
2939
# Vault Password
3040

3141
def get_vault_password(password_file=None):
@@ -73,8 +83,8 @@ def split_path(path):
7383
parts = []
7484
path, tail = os.path.split(path)
7585
while path and tail:
76-
parts.append(tail)
77-
path, tail = os.path.split(path)
86+
parts.append(tail)
87+
path, tail = os.path.split(path)
7888
parts.append(os.path.join(path, tail))
7989
return map(os.path.normpath, parts)[::-1]
8090

bin/atk-git-diff

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env python
2+
3+
import argparse
4+
import subprocess
5+
import sys
6+
7+
from ansible_toolkit.git_diff import show_unencrypted_diffs
8+
9+
10+
if __name__ == '__main__':
11+
parser = argparse.ArgumentParser()
12+
parser.add_argument('-p', '--vault-password-file', type=str,
13+
help="Path to vault password file")
14+
args = parser.parse_args()
15+
output = subprocess.check_output(['git', 'diff'])
16+
show_unencrypted_diffs(output, args.vault_password_file)

img/git-diff-encrypted.png

36.9 KB
Loading

img/git-diff-unencrypted.png

16.1 KB
Loading

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
setup(name='ansible-toolkit',
5-
version='1.2.3',
5+
version='1.3.0',
66
description='The missing Ansible tools',
77
url='http://github.com/dellis23/ansible-toolkit',
88
author='Daniel Ellis',
@@ -11,6 +11,7 @@
1111
install_requires=['ansible'],
1212
packages=['ansible_toolkit'],
1313
scripts=[
14+
'bin/atk-git-diff',
1415
'bin/atk-show-vars',
1516
'bin/atk-show-template',
1617
'bin/atk-vault',

0 commit comments

Comments
 (0)