Repository: Lucas-C/pre-commit-hooks

Issue:
Trailing newline handling in insert-license
Currently there is a rather annoying issue in how a LICENCE.txt file with a trailing newline is handled.

Our `LICENSE.txt` contains, in abstract, with newlines made explicit:

```
line1\n
line2\n
line3\n
```

so the last line ends in a newline. Nothing unusual here.

But this causes the hook to want to insert license headers **twice** when using a three-element comment style header, like `/*| *| */`. 

That's because the above `LICENSE.txt` content is then transformed to the `prefixed_license` list:

```python
['/*\n',
 ' * line1\n',
 ' * line2\n',
 ' * line3\n',
 '\n */']
```

Note the `\n` character on the last line. The first time you add a licence header things go sort-of fine, and the files end up with

```
/*\n
 * line1\n
 * line2\n
 * line3\n
\n
 */\n

File contents start here
```

Note the extra, empty newline in the header. That by itself is a little irksome, we didn't need an extra newline there.

But next time such a file is modified at the commit hook runs again, you now can't detect that the header is *already present*, because now the `find_license_header_index()` function fails to account for the extra newline. There isn't an extra element in the `prefixed_license` for that line, it's a prefixed newline in the wrong place, the start of an entry.

That's because the source lines `\n` (the empty line inserted by the script), and `\n #}` (the line in the `prefixed_license` list) can never line up. The newline exists in `prefixed_license`, but not on its own.

The work-around is to commit a LICENSE.txt file with no trailing newline.

A proper fix would add the newline to the last line of the license lines only if there isn't one there already, rather than prefix `comment_end`. `comment_end` itself also needs a newline character though:

```python
if not prefixed_license[-1].endswith(eol):
    prefixed_license[-1] += eol  # make sure the license text ends in a newline
if comment_start:
    prefixed_license = [comment_start + eol] + prefixed_license
if comment_end:
    prefixed_license = prefixed_license + [comment_end + eol]
```

Comments:

Patch:
diff --git a/pre_commit_hooks/insert_license.py b/pre_commit_hooks/insert_license.py
index ecd7dfa..8e77863 100755
--- a/pre_commit_hooks/insert_license.py
+++ b/pre_commit_hooks/insert_license.py
@@ -22,10 +22,12 @@ def main(argv=None):
         prefixed_license = ['{}{}{}'.format(comment_prefix, ' ' if line.strip() else '', line)
                             for line in license_file.readlines()]
     eol = '\r\n' if prefixed_license[0][-2:] == '\r\n' else '\n'
+    if not prefixed_license[-1].endswith(eol):
+        prefixed_license[-1] += eol
     if comment_start:
         prefixed_license = [comment_start + eol] + prefixed_license
     if comment_end:
-        prefixed_license = prefixed_license + [eol + comment_end]
+        prefixed_license = prefixed_license + [comment_end + eol]
 
     changes_made = False
     for src_filepath in args.filenames:
@@ -42,7 +44,7 @@ def main(argv=None):
                     src_file.write(''.join(src_file_content))
                 changes_made = True
         elif not args.remove_header:
-            src_file_content = prefixed_license + [eol + eol] + src_file_content
+            src_file_content = prefixed_license + [eol] + src_file_content
             with open(src_filepath, 'w') as src_file:
                 src_file.write(''.join(src_file_content))
             changes_made = True


Test patch:
diff --git a/tests/insert_license_test.py b/tests/insert_license_test.py
index 06522c7..6743d97 100755
--- a/tests/insert_license_test.py
+++ b/tests/insert_license_test.py
@@ -1,6 +1,7 @@
 from __future__ import unicode_literals
 
 from contextlib import contextmanager
+from itertools import product
 import os, pytest, shutil
 
 from pre_commit_hooks.insert_license import main as insert_license
@@ -8,20 +9,26 @@ from pre_commit_hooks.insert_license import find_license_header_index
 
 
 @pytest.mark.parametrize(
-    ('src_file_path', 'comment_prefix', 'new_src_file_expected'),
-    (
-        ('module_without_license.py', '#', 'module_with_license.py'),
-        ('module_with_license.py', '#', False),
-        ('module_with_license_and_shebang.py', '#', False),
-        ('module_without_license.groovy', '//', 'module_with_license.groovy'),
-        ('module_with_license.groovy', '//', False),
-    ),
+    ('license_file_path', 'src_file_path', 'comment_prefix', 'new_src_file_expected'),
+    map(lambda a: a[:1] + a[1], product(  # combine license files with other args
+        ('LICENSE_with_trailing_newline.txt', 'LICENSE_without_trailing_newline.txt'),
+        (
+            ('module_without_license.py', '#', 'module_with_license.py'),
+            ('module_with_license.py', '#', False),
+            ('module_with_license_and_shebang.py', '#', False),
+            ('module_without_license.groovy', '//', 'module_with_license.groovy'),
+            ('module_with_license.groovy', '//', False),
+            ('module_without_license.css', '/*| *| */', 'module_with_license.css'),
+            ('module_with_license.css', '/*| *| */', False),
+        ),
+    )),
 )
-def test_insert_license(src_file_path, comment_prefix, new_src_file_expected, tmpdir):
+def test_insert_license(license_file_path, src_file_path, comment_prefix, new_src_file_expected, tmpdir):
     with chdir_to_test_resources():
         path = tmpdir.join('src_file_path')
         shutil.copy(src_file_path, path.strpath)
-        assert insert_license(['--comment-style', comment_prefix, path.strpath]) == (1 if new_src_file_expected else 0)
+        args = ['--license-filepath', license_file_path, '--comment-style', comment_prefix, path.strpath]
+        assert insert_license(args) == (1 if new_src_file_expected else 0)
         if new_src_file_expected:
             with open(new_src_file_expected) as expected_content_file:
                 expected_content = expected_content_file.read()
@@ -42,18 +49,21 @@ def test_is_license_present(src_file_content, expected_index):
 
 
 @pytest.mark.parametrize(
-    ('src_file_path', 'is_python', 'new_src_file_expected'),
-    (
-        ('module_with_license.css', False, 'module_without_license.css'),
-        ('module_without_license.css', False, False),
-        ('module_with_license_and_shebang.py', True, 'module_without_license_and_shebang.py'),
-    ),
+    ('license_file_path', 'src_file_path', 'is_python', 'new_src_file_expected'),
+    map(lambda a: a[:1] + a[1], product(  # combine license files with other args
+        ('LICENSE_with_trailing_newline.txt', 'LICENSE_without_trailing_newline.txt'),
+        (
+            ('module_with_license.css', False, 'module_without_license.css'),
+            ('module_without_license.css', False, False),
+            ('module_with_license_and_shebang.py', True, 'module_without_license_and_shebang.py'),
+        ),
+    )),
 )
-def test_remove_license(src_file_path, is_python, new_src_file_expected, tmpdir):
+def test_remove_license(license_file_path, src_file_path, is_python, new_src_file_expected, tmpdir):
     with chdir_to_test_resources():
         path = tmpdir.join('src_file_path')
         shutil.copy(src_file_path, path.strpath)
-        argv = ['--remove-header', path.strpath]
+        argv = ['--license-filepath', license_file_path, '--remove-header', path.strpath]
         if is_python:
             argv = ['--comment-style', '#'] + argv
         else:
diff --git a/tests/resources/LICENSE_with_trailing_newline.txt b/tests/resources/LICENSE_with_trailing_newline.txt
new file mode 100755
index 0000000..cbbe3c0
--- /dev/null
+++ b/tests/resources/LICENSE_with_trailing_newline.txt
@@ -0,0 +1,3 @@
+Copyright (C) 2017 Teela O'Malley
+
+Licensed under the Apache License, Version 2.0 (the "License");