Skip to content

Commit

Permalink
Add atk-git-diff.
Browse files Browse the repository at this point in the history
  • Loading branch information
Dan Ellis committed Jun 17, 2015
1 parent 2b19d5e commit 3d5a601
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 5 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.sw?
*.egg-info*
*.pyc
/dist/
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ It's important that the vault always be opened and closed from the
base directory of your ansible project. Newer versions may attempt
to detect and force this by default.

### atk-git-diff ###

Doing a `git diff` on encrypted files produces some pretty useless output.
`atk-git-diff` will detect changes via `git diff` unencrypt the before and
after and then show the difference.

This:

![Encrypted git diff output](https://github.com/dellis23/ansible-toolkit/blob/master/img/git-diff-encrypted.png)

Becomes:

![Unencrypted git diff output](https://github.com/dellis23/ansible-toolkit/blob/master/img/git-diff-unencrypted.png)


Contributing
------------

Expand All @@ -105,6 +120,10 @@ to make it work for more environments.
Changelog
---------

### 1.3.0 ###

`atk-git-diff` added.

### 1.2.3 ###

Add ability to specify vault password file and inventory file on the command
Expand Down
127 changes: 127 additions & 0 deletions ansible_toolkit/git_diff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import difflib
from itertools import islice
import re
import subprocess

from ansible.utils.vault import VaultLib

from utils import get_vault_password, green, red, cyan, intense


def get_parts(git_diff_output):
r = re.compile(r"^diff --git", re.MULTILINE)
parts = []
locations = [i.start() for i in r.finditer(git_diff_output)]
for i, location in enumerate(locations):
is_last_item = i + 1 == len(locations)
next_location = None if is_last_item else locations[i + 1]
parts.append(git_diff_output[location:next_location])
return parts


def get_old_sha(diff_part):
"""
Returns the SHA for the original file that was changed in a diff part.
"""
r = re.compile(r'index ([a-fA-F\d]*)')
return r.search(diff_part).groups()[0]


def get_old_filename(diff_part):
"""
Returns the filename for the original file that was changed in a diff part.
"""
r = re.compile(r'^--- a/(.*)', re.MULTILINE)
return r.search(diff_part).groups()[0]


def get_old_contents(sha, filename):
return subprocess.check_output(['git', 'show', sha, '--', filename])


def get_new_filename(diff_part):
"""
Returns the filename for the updated file in a diff part.
"""
r = re.compile(r'^\+\+\+ b/(.*)', re.MULTILINE)
return r.search(diff_part).groups()[0]


def get_new_contents(filename):
with open(filename, 'rb') as f:
return f.read()


def get_head(diff_part):
"""
Returns the pre-content, non-chunk headers of a diff part.
E.g.
diff --git a/group_vars/foo b/group_vars/foo
index 6b9eef7..eb9fb09 100644
--- a/group_vars/foo
+++ b/group_vars/foo
"""
return '\n'.join(diff_part.split('\n')[:4]) + '\n'


def get_contents(diff_part):
"""
Returns a tuple of old content and new content.
"""
old_sha = get_old_sha(diff_part)
old_filename = get_old_filename(diff_part)
old_contents = get_old_contents(old_sha, old_filename)
new_filename = get_new_filename(diff_part)
new_contents = get_new_contents(new_filename)
return old_contents, new_contents


def decrypt_diff(diff_part, password_file=None):
"""
Diff part is a string in the format:
diff --git a/group_vars/foo b/group_vars/foo
index c09080b..0d803bb 100644
--- a/group_vars/foo
+++ b/group_vars/foo
@@ -1,32 +1,33 @@
$ANSIBLE_VAULT;1.1;AES256
-61316662363730313230626432303662316330323064373866616436623565613033396539366263
-383632656663356364656531653039333965
+30393563383639396563623339383936613866326332383162306532653239636166633162323236
+62376161626137626133
Returns a tuple of decrypted old contents and decrypted new contents.
"""
vault = VaultLib(get_vault_password(password_file))
old_contents, new_contents = get_contents(diff_part)
if vault.is_encrypted(old_contents):
old_contents = vault.decrypt(old_contents)
if vault.is_encrypted(new_contents):
new_contents = vault.decrypt(new_contents)
return old_contents, new_contents


def show_unencrypted_diff(diff_part, password_file=None):
intense(get_head(diff_part).strip())
old, new = decrypt_diff(diff_part, password_file)
diff = difflib.unified_diff(old.split('\n'), new.split('\n'), lineterm='')
# ... we'll take the git filenames from git's diff output rather than
# ... difflib
for line in islice(diff, 2, None):
if line.startswith('-'):
red(line)
elif line.startswith('+'):
green(line)
elif line.startswith('@@'):
cyan(line)
else:
print line


def show_unencrypted_diffs(git_diff_output, password_file=None):
parts = get_parts(git_diff_output)
for part in parts:
show_unencrypted_diff(part, password_file)
119 changes: 119 additions & 0 deletions ansible_toolkit/tests/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import tempfile
import unittest

from ansible_toolkit import git_diff


VAULT_PASSWORD = "foo"


# Vaulted Files

OLD_FILE_1 = """$ANSIBLE_VAULT;1.1;AES256
35626233663138316665643331653539633239313534376130633265353137313531656664613539
6466346666333332636231356362323834616462323161610a613533363462663730366462633466
36303466376432323137383661653036663338343830666264343562643833313931616165653463
6637633063376664360a643964336134383034343665383730623064353338366436326135366265
3038"""

OLD_FILE_1_DECRYPTED = """foo
"""

NEW_FILE_1 = """$ANSIBLE_VAULT;1.1;AES256
39373663313137383662393466386133396438366235323234336365623535363133353631366566
3565616539353938343863633635626334636532393936390a353430663164393034616161356237
34386335363662623266646637653135613337666636323834313764303766306131663334336436
6633623031353931640a626366353232613963303939303130313132666437346163363535663265
6535"""

NEW_FILE_1_DECRYPTED = """bar
"""


# Git Diff Output

DIFF_HEAD_1 = """diff --git a/group_vars/foo b/group_vars/foo
index c09080b..0d803bb 100644
--- a/group_vars/foo
+++ b/group_vars/foo
"""

SAMPLE_DIFF = DIFF_HEAD_1

SAMPLE_DIFF += """@@ -1,32 +1,33 @@
ANSIBLE_VAULT;1.1;AES256"""

SAMPLE_DIFF += ''.join('\n-' + i for i in OLD_FILE_1.split('\n')[1:])
SAMPLE_DIFF += ''.join('\n+' + i for i in NEW_FILE_1.split('\n')[1:])

# ... another section to make sure we can handle multiple parts

SAMPLE_DIFF += """
diff --git a/group_vars/bar b/group_vars/bar
index 6b9eef7..eb9fb09 100644
--- a/group_vars/bar
+++ b/group_vars/bar
@@ -1,22 +1,23 @@
$ANSIBLE_VAULT;1.1;AES256
-32346330646639326335373939383634656365376531353531306238616239626265313963613561
-61393637373834646566353739393762306436393234636438323434626666366136
+65393432336536653066303736336632356364306533643131656461316332353138316239336137
+3038396139303439356236343161396331353332326232626566"""


class TestGitDiff(unittest.TestCase):

def test_get_parts(self):
parts = git_diff.get_parts(SAMPLE_DIFF)
self.assertEqual(len(parts), 2)

def test_get_old_sha(self):
parts = git_diff.get_parts(SAMPLE_DIFF)
old_sha = git_diff.get_old_sha(parts[0])
self.assertEqual(old_sha, 'c09080b')

def test_get_old_filename(self):
parts = git_diff.get_parts(SAMPLE_DIFF)
old_filename = git_diff.get_old_filename(parts[0])
self.assertEqual(old_filename, 'group_vars/foo')

def test_decrypt_diff(self):

# Monkey-patch
_get_old_contents = git_diff.get_old_contents
def get_old_contents(*args): # noqa
return OLD_FILE_1
git_diff.get_old_contents = get_old_contents
_get_new_contents = git_diff.get_new_contents
def get_new_contents(*args): # noqa
return NEW_FILE_1
git_diff.get_new_contents = get_new_contents

# Test decryption
try:

# ... create temporary vault file
f = tempfile.NamedTemporaryFile()
f.write(VAULT_PASSWORD)
f.seek(0)

# ... decrypt the diff
parts = git_diff.get_parts(SAMPLE_DIFF)
old, new = git_diff.decrypt_diff(
parts[0], password_file=f.name)
self.assertEqual(old, OLD_FILE_1_DECRYPTED)
self.assertEqual(new, NEW_FILE_1_DECRYPTED)

# Restore monkey-patched functions
finally:
git_diff.get_old_contents = _get_old_contents
git_diff.get_new_contents = _get_new_contents

def test_get_head(self):
parts = git_diff.get_parts(SAMPLE_DIFF)
head = git_diff.get_head(parts[0])
self.assertEqual(head, DIFF_HEAD_1)


if __name__ == '__main__':
unittest.main()
18 changes: 14 additions & 4 deletions ansible_toolkit/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@

# Terminal Colors

GREEN = '\033[92m'
RED = '\033[91m'
RED = '\033[31m'
GREEN = '\033[32m'
CYAN = '\033[36m'
INTENSE = '\033[1m'
ENDC = '\033[0m'


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


def cyan(text):
print CYAN + text + ENDC


def intense(text):
print INTENSE + text + ENDC


# Vault Password

def get_vault_password(password_file=None):
Expand Down Expand Up @@ -73,8 +83,8 @@ def split_path(path):
parts = []
path, tail = os.path.split(path)
while path and tail:
parts.append(tail)
path, tail = os.path.split(path)
parts.append(tail)
path, tail = os.path.split(path)
parts.append(os.path.join(path, tail))
return map(os.path.normpath, parts)[::-1]

Expand Down
16 changes: 16 additions & 0 deletions bin/atk-git-diff
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env python

import argparse
import subprocess
import sys

from ansible_toolkit.git_diff import show_unencrypted_diffs


if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-p', '--vault-password-file', type=str,
help="Path to vault password file")
args = parser.parse_args()
output = subprocess.check_output(['git', 'diff'])
show_unencrypted_diffs(output, args.vault_password_file)
Binary file added img/git-diff-encrypted.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/git-diff-unencrypted.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@


setup(name='ansible-toolkit',
version='1.2.3',
version='1.3.0',
description='The missing Ansible tools',
url='http://github.com/dellis23/ansible-toolkit',
author='Daniel Ellis',
Expand All @@ -11,6 +11,7 @@
install_requires=['ansible'],
packages=['ansible_toolkit'],
scripts=[
'bin/atk-git-diff',
'bin/atk-show-vars',
'bin/atk-show-template',
'bin/atk-vault',
Expand Down

0 comments on commit 3d5a601

Please sign in to comment.