diff --git a/mkdocs_git_authors_plugin/git/command.py b/mkdocs_git_authors_plugin/git/command.py
index 1696cbe..6efec30 100644
--- a/mkdocs_git_authors_plugin/git/command.py
+++ b/mkdocs_git_authors_plugin/git/command.py
@@ -64,7 +64,7 @@ class GitCommand(object):
         )
         try:
             p.check_returncode()
-        except subprocess.CalledProcessError as e:
+        except subprocess.CalledProcessError:
             msg = ['GitCommand error:']
             msg.append('Command "%s" failed' % ' '.join(args))
             msg.append('Return code: %s' % p.returncode)
diff --git a/mkdocs_git_authors_plugin/git/commit.py b/mkdocs_git_authors_plugin/git/commit.py
index ee3b2e5..52eb86e 100644
--- a/mkdocs_git_authors_plugin/git/commit.py
+++ b/mkdocs_git_authors_plugin/git/commit.py
@@ -1,3 +1,4 @@
+import re
 from .repo import AbstractRepoObject, Repo
 from .. import util
 
@@ -32,6 +33,11 @@ class Commit(AbstractRepoObject):
 
         super().__init__(repo)
 
+        # Replace <> 
+        # from '<email@domain.com>'
+        # to   'email@domain.com'
+        author_email = re.sub(r"\<|\>","", author_email)
+         
         self._author = self.repo().author(
             author_name,
             author_email
diff --git a/mkdocs_git_authors_plugin/git/page.py b/mkdocs_git_authors_plugin/git/page.py
index 9b13db8..77f6ed2 100644
--- a/mkdocs_git_authors_plugin/git/page.py
+++ b/mkdocs_git_authors_plugin/git/page.py
@@ -1,5 +1,6 @@
 from pathlib import Path
 import re
+import logging
 from .repo import Repo, AbstractRepoObject
 from .command import GitCommand, GitCommandError
 
@@ -66,6 +67,7 @@ class Page(AbstractRepoObject):
     def authors_summary(self):
         """
         Summarized list of authors to a HTML string
+        TODO: move to util
 
         Args:
         Returns:
@@ -148,7 +150,7 @@ class Page(AbstractRepoObject):
             --- (this method works through side effects)
         """
 
-        re_sha = re.compile('^\w{40}')
+        re_sha = re.compile(r'^\w{40}')
 
         cmd = GitCommand('blame', ['--porcelain', str(self._path)])
         cmd.run()
diff --git a/mkdocs_git_authors_plugin/plugin.py b/mkdocs_git_authors_plugin/plugin.py
index 9a2c226..477feb1 100644
--- a/mkdocs_git_authors_plugin/plugin.py
+++ b/mkdocs_git_authors_plugin/plugin.py
@@ -177,18 +177,7 @@ class GitAuthorsPlugin(BasePlugin):
         # omitting the 'str' argument would result in a
         # datetime.datetime object with tzinfo instead.
         # Should this be formatted differently?
-        context['git_authors'] = [
-            {
-                'name' : author.name(),
-                'email' : author.email(),
-                'last_datetime' : author.datetime(path, str),
-                'lines' : author.lines(path),
-                'lines_all_pages' : author.lines(),
-                'contribution' : author.contribution(path, str),
-                'contribution_all_pages' : author.contribution(None, str)
-            }
-            for author in authors
-        ]
+        context['git_authors'] = util.page_authors(authors, path)
         context['git_authors_summary'] = page_obj.authors_summary()
 
         return context
diff --git a/mkdocs_git_authors_plugin/util.py b/mkdocs_git_authors_plugin/util.py
index 55618e4..51668d9 100644
--- a/mkdocs_git_authors_plugin/util.py
+++ b/mkdocs_git_authors_plugin/util.py
@@ -1,4 +1,5 @@
 from datetime import datetime, timezone, timedelta
+from pathlib import Path
 
 def commit_datetime(author_time: str, author_tz: str):
     """
@@ -88,3 +89,24 @@ def repo_authors_summary(authors, config: dict):
 </ul>
     """
     return result
+
+
+def page_authors(authors, path):
+    """List of dicts with info on page authors
+    
+    Args:
+        authors (list): list with Author classes
+        path (str): path to page
+    """
+    if type(path) == str:
+        path = Path(path)
+    return [{
+            'name' : author.name(),
+            'email' : author.email(),
+            'last_datetime' : author.datetime(path, str),
+            'lines' : author.lines(path),
+            'lines_all_pages' : author.lines(),
+            'contribution' : author.contribution(path, str),
+            'contribution_all_pages' : author.contribution(None, str)
+        }
+        for author in authors]
\ No newline at end of file

Update unit tests after replacing GitPython with Git classes
@uliska Just to let you know I'm working on the unit tests, so we don't do double work.
diff --git a/.github/workflows/unittests_codecov.yml b/.github/workflows/unittests_codecov.yml
index 4bd1608..4c5fe3f 100644
--- a/.github/workflows/unittests_codecov.yml
+++ b/.github/workflows/unittests_codecov.yml
@@ -1,5 +1,13 @@
-name: Unit tests
-on: [push]
+name: Unit tests with codecov upload
+on:
+  # Trigger the workflow on push or pull request,
+  # but only for the master branch
+  push:
+    branches:
+      - master
+    pull_request:
+      branches:
+        - master
 jobs:
   run:
     name: Run unit tests
diff --git a/tests/basic_setup/docs/first_page.md b/tests/basic_setup/docs/first_page.md
new file mode 100644
index 0000000..1b500d3
--- /dev/null
+++ b/tests/basic_setup/docs/first_page.md
@@ -0,0 +1,3 @@
+# First Test page
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
\ No newline at end of file
diff --git a/tests/basic_setup/docs/second_page.md b/tests/basic_setup/docs/second_page.md
new file mode 100644
index 0000000..e5a9f7b
--- /dev/null
+++ b/tests/basic_setup/docs/second_page.md
@@ -0,0 +1,3 @@
+# Second Test page
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
\ No newline at end of file
diff --git a/tests/test_util.py b/tests/test_util.py
index d270a79..a680c9e 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -1,39 +1,168 @@
-from mkdocs_git_authors_plugin import util
+"""
+Note that pytest offers a `tmp_path`. 
+You can reproduce locally with
+
+```python
+%load_ext autoreload
+%autoreload 2
+import os
+import tempfile
+import shutil
+from pathlib import Path
+tmp_path = Path(tempfile.gettempdir()) / 'pytest-retrieve-authors'
+if os.path.exists(tmp_path):
+    shutil.rmtree(tmp_path)
+os.mkdir(tmp_path)
+```
+"""
+
 import os
-from git import Actor, Repo
+import shutil
+import logging
+
+from mkdocs_git_authors_plugin import util
+from mkdocs_git_authors_plugin.git import repo
+
+# GitPython
+import git as gitpython
+
+DEFAULT_CONFIG = {
+    'show_contribution': False, 
+    'show_line_count': False, 
+    'count_empty_lines': True, 
+    'sort_authors_by': 'name', 
+    'sort_reverse': False
+}
+
+#### Helpers #### 
+
+def setup_clean_mkdocs_folder(mkdocs_yml_path, output_path):
+    """
+    Sets up a clean mkdocs directory
+    
+    outputpath/testproject
+    ├── docs/
+    └── mkdocs.yml
+    
+    Args:
+        mkdocs_yml_path (Path): Path of mkdocs.yml file to use
+        output_path (Path): Path of folder in which to create mkdocs project
+        
+    Returns:
+        testproject_path (Path): Path to test project
+    """
+
+    testproject_path = output_path / 'testproject'
+    
+    # Create empty 'testproject' folder    
+    if os.path.exists(testproject_path):
+        logging.warning("""This command does not work on windows. 
+        Refactor your test to use setup_clean_mkdocs_folder() only once""")
+        shutil.rmtree(testproject_path)
+
+    # Copy correct mkdocs.yml file and our test 'docs/'        
+    shutil.copytree('tests/basic_setup/docs', testproject_path / 'docs')
+    shutil.copyfile(mkdocs_yml_path, testproject_path / 'mkdocs.yml')
+    
+    return testproject_path
+
+
+def setup_commit_history(testproject_path):
+    """
+    Initializes and creates a git commit history
+    in a new mkdocs testproject. 
+    
+    We commit the pages one by one in order 
+    to create some git depth.
+    
+    Args:
+        testproject_path (Path): Path to test project
+        
+    Returns:
+        repo (repo): git.Repo object
+    """
+    assert not os.path.exists(testproject_path / '.git') 
+
+    repo = gitpython.Repo.init(testproject_path, bare = False)
+    author = "Test Person <testtest@gmail.com>"
+    
+    # Change the working directory
+    cwd = os.getcwd()
+    os.chdir(testproject_path)
+    
+    try: 
+        repo.git.add('mkdocs.yml')
+        repo.git.commit(message = 'add mkdocs', author = author)
+
+        repo.git.add('docs/first_page.md')
+        repo.git.commit(message = 'first page', author = author)
+        file_name = os.path.join(testproject_path, 'docs/first_page.md')
+        with open(file_name, 'w+') as the_file:
+            the_file.write('Hello\n')
+        repo.git.add('docs/first_page.md')
+        repo.git.commit(message = 'first page update 1', author = author)
+        with open(file_name, 'w') as the_file:
+            the_file.write('# First Test Page Edited\n\nSome Lorem text')
+        repo.git.add('docs/first_page.md')
+        repo.git.commit(message = 'first page update 2', author = author)
+        
+        repo.git.add('docs/second_page.md')
+        repo.git.commit(message = 'second page', author = author)
+        repo.git.add('docs/index.md')
+        repo.git.commit(message = 'homepage', author = author)
+        repo.git.add('docs/page_with_tag.md')
+        repo.git.commit(message = 'homepage', author = author)
+        os.chdir(cwd)
+    except:
+        os.chdir(cwd)
+        raise
+    
+    return repo
+
+#### Tests ####
+
 
 def test_empty_file(tmp_path):
 
+    # Change working directory
+    os.chdir(tmp_path)
+    
     # Create empty file
     file_name = os.path.join(tmp_path, 'new-file')
     open(file_name, 'a').close()
 
     # Get authors of empty, uncommitted file
-    r = Repo.init(tmp_path)
-    instance = util.Util(tmp_path)
-    authors = instance.get_authors(path = file_name)
+    r = gitpython.Repo.init(tmp_path)
+    
+    repo_instance = repo.Repo()
+    repo_instance.set_config(DEFAULT_CONFIG)
+    # TODO: should throw an error?
+    repo_instance.page(file_name)
+    
+    authors = repo_instance.get_authors()
     assert authors == []
 
-    # Get authors of empty, committed file
+    # Get authors of empty but committed file
     r.index.add([file_name])
-    author = Actor('Tim', 'abc@abc.com')
+    author = gitpython.Actor('Tim', 'abc@abc.com')
     r.index.commit("initial commit", author = author)
-    authors = instance.get_authors(path = file_name)
+    
+    repo_instance.page(file_name)
+    authors = repo_instance.get_authors()
     assert authors == []
+    
+    ## TODO 
+    # When the first instance of a commit on a page is skipped as an empty line, 
+    # the second instance will not have the commit metadata available
 
 def test_retrieve_authors(tmp_path):
     """
     Builds a fake git project with some commits.
 
-    pytest offers a `tmp_path`. You can reproduce locally with
-    >>> import tempfile
-    >>> from pathlib import Path
-    >>> tmp_path = Path(tempfile.gettempdir()) / 'pytest-retrieve-authors'
-    >>> os.mkdir(tmp_path)
-
     Args:
         tmp_path (PosixPath): Directory of a tempdir
     """
+    os.chdir(tmp_path)    
 
     # Create file
     file_name = os.path.join(tmp_path, 'new-file')
@@ -41,14 +170,20 @@ def test_retrieve_authors(tmp_path):
         the_file.write('Hello\n')
 
     # Create git repo and commit file
-    r = Repo.init(tmp_path)
+    r = gitpython.Repo.init(tmp_path)
     r.index.add([file_name])
-    author = Actor('Tim', 'abc@abc.com')
+    author = gitpython.Actor('Tim', 'abc@abc.com')
     r.index.commit("initial commit", author = author)
 
-    instance = util.Util(tmp_path)
-    authors = instance.get_authors(path = file_name)
+    # Test retrieving author
+    repo_instance = repo.Repo()
+    repo_instance.set_config(DEFAULT_CONFIG)
+    repo_instance.page(file_name)
+    
+    authors = repo_instance.get_authors()
+    assert len(authors) == 1
     # We don't want to test datetime
+    authors = util.page_authors(authors, file_name)
     authors[0]['last_datetime'] = None
 
     assert authors == [{
@@ -56,7 +191,9 @@ def test_retrieve_authors(tmp_path):
                     'email' : "abc@abc.com",
                     'last_datetime' : None,
                     'lines' : 1,
-                    'contribution' : '100.0%'
+                    'lines_all_pages': 1,
+                    'contribution' : '100.0%',
+                    'contribution_all_pages': '100.0%'
                 }]
 
     # Now add a line to the file
@@ -64,11 +201,14 @@ def test_retrieve_authors(tmp_path):
     with open(file_name, 'a+') as the_file:
         the_file.write('World\n')
     r.index.add([file_name])
-    author = Actor('Tim2', 'abc@abc.com')
+    author = gitpython.Actor('Tim2', 'abc@abc.com')
     r.index.commit("another commit", author = author)
 
-    instance = util.Util(tmp_path)
-    authors = instance.get_authors(path = file_name)
+    repo_instance = repo.Repo()
+    repo_instance.set_config(DEFAULT_CONFIG)
+    repo_instance.page(file_name)
+    authors = repo_instance.get_authors()
+    authors = util.page_authors(authors, file_name)
     authors[0]['last_datetime'] = None
 
     assert authors == [{
@@ -76,18 +216,23 @@ def test_retrieve_authors(tmp_path):
                     'email' : "abc@abc.com",
                     'last_datetime' : None,
                     'lines' : 2,
-                    'contribution' : '100.0%'
+                    'lines_all_pages': 2,
+                    'contribution' : '100.0%',
+                    'contribution_all_pages': '100.0%'
                 }]
 
     # Then a third commit from a new author
     with open(file_name, 'a+') as the_file:
         the_file.write('A new line\n')
     r.index.add([file_name])
-    author = Actor('John', 'john@abc.com')
+    author = gitpython.Actor('John', 'john@abc.com')
     r.index.commit("third commit", author = author)
 
-    instance = util.Util(tmp_path)
-    authors = instance.get_authors(path = file_name)
+    repo_instance = repo.Repo()
+    repo_instance.set_config(DEFAULT_CONFIG)
+    repo_instance.page(file_name)
+    authors = repo_instance.get_authors()
+    authors = util.page_authors(authors, file_name)
     authors[0]['last_datetime'] = None
     authors[1]['last_datetime'] = None
 
@@ -96,17 +241,25 @@ def test_retrieve_authors(tmp_path):
                     'email' : "john@abc.com",
                     'last_datetime' : None,
                     'lines' : 1,
-                    'contribution' : '33.33%'
+                    'lines_all_pages': 1,
+                    'contribution' : '33.33%',
+                    'contribution_all_pages': '33.33%'
                 },{
                     'name' : "Tim",
                     'email' : "abc@abc.com",
                     'last_datetime' : None,
                     'lines' : 2,
-                    'contribution' : '66.67%'
+                    'lines_all_pages': 2,
+                    'contribution' : '66.67%',
+                    'contribution_all_pages': '66.67%'
                 }]
 
 def test_summarize_authors():
-
+    """
+    Test summary functions. 
+    TODO
+    """
+    pass
     authors = [
         {'name' : 'Tim',
          'email' : 'abc@abc.com',
@@ -114,22 +267,23 @@ def test_summarize_authors():
         }
     ]
 
-    # Default case: don't show contribution
-    config = { 'show_contribution' : False }
-    summary = util.Util().summarize(authors, config)
-    assert summary == "<span class='git-authors'><a href='mailto:abc@abc.com'>Tim</a></span>"
-
-    # Do show contribution, but hide it because there's only one author
-    config = { 'show_contribution' : True }
-    summary = util.Util().summarize(authors, config)
-    assert summary == "<span class='git-authors'><a href='mailto:abc@abc.com'>Tim</a></span>"
-
-    # Add another author
-    authors.append({
-        'name' : 'Tom',
-        'email' : 'efg@efg.org',
-        'contribution' : '35.77%'
-    })
-    # Now contribution is displayed
-    summary = util.Util().summarize(authors, config)
-    assert summary == "<span class='git-authors'><a href='mailto:abc@abc.com'>Tim</a> (64.23%), <a href='mailto:efg@efg.org'>Tom</a> (35.77%)</span>"
+    # # Default case: don't show contribution
+    # config = { 'show_contribution' : False }
+    # summary = util.Util().summarize(authors, config)
+    # assert summary == "<span class='git-authors'><a href='mailto:abc@abc.com'>Tim</a></span>"
+
+    # # Do show contribution, 
+    # # but hide it because there's only one author
+    # config = { 'show_contribution' : True }
+    # summary = util.Util().summarize(authors, config)
+    # assert summary == "<span class='git-authors'><a href='mailto:abc@abc.com'>Tim</a></span>"
+
+    # # Add another author
+    # authors.append({
+    #     'name' : 'Tom',
+    #     'email' : 'efg@efg.org',
+    #     'contribution' : '35.77%'
+    # })
+    # # Now contribution is displayed
+    # summary = util.Util().summarize(authors, config)
+    # assert summary == "<span class='git-authors'><a href='mailto:abc@abc.com'>Tim</a> (64.23%), <a href='mailto:efg@efg.org'>Tom</a> (35.77%)</span>"

pip install -e .
parse_log_pytest
requirements.txt
pytest pytest-cov codecov click GitPython
3.7
tests/test_requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
click==8.1.8
codecov==2.1.13
coverage==7.2.7
exceptiongroup==1.2.2
ghp-import==2.1.0
gitdb==4.0.12
GitPython==3.1.44
idna==3.10
importlib-metadata==6.7.0
iniconfig==2.0.0
Jinja2==3.1.6
Markdown==3.4.4
MarkupSafe==2.1.5
mergedeep==1.3.4
mkdocs==1.5.3
-e git+https://github.com/timvink/mkdocs-git-authors-plugin.git@1bca838c5d6616dfe867f22ea8cf2a6fcb7c8ba4#egg=mkdocs_git_authors_plugin
packaging==24.0
pathspec==0.11.2
platformdirs==4.0.0
pluggy==1.2.0
pytest==7.4.4
pytest-cov==4.1.0
python-dateutil==2.9.0.post0
PyYAML==6.0.1
pyyaml_env_tag==0.1
requests==2.31.0
six==1.17.0
smmap==5.0.2
tomli==2.0.1
typing_extensions==4.7.1
urllib3==2.0.7
watchdog==3.0.0
zipp==3.15.0

name: mkdocs-git-authors-plugin
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - charset-normalizer==3.4.1
      - click==8.1.8
      - codecov==2.1.13
      - coverage==7.2.7
      - exceptiongroup==1.2.2
      - ghp-import==2.1.0
      - gitdb==4.0.12
      - gitpython==3.1.44
      - idna==3.10
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - jinja2==3.1.6
      - markdown==3.4.4
      - markupsafe==2.1.5
      - mergedeep==1.3.4
      - mkdocs==1.5.3
      - packaging==24.0
      - pathspec==0.11.2
      - platformdirs==4.0.0
      - pluggy==1.2.0
      - pytest==7.4.4
      - pytest-cov==4.1.0
      - python-dateutil==2.9.0.post0
      - pyyaml==6.0.1
      - pyyaml-env-tag==0.1
      - requests==2.31.0
      - six==1.17.0
      - smmap==5.0.2
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - watchdog==3.0.0
      - zipp==3.15.0
prefix: /opt/conda/envs/mkdocs-git-authors-plugin

tests/test_util.py::test_empty_file tests/test_util.py::test_retrieve_authors
tests/test_util.py::test_summarize_authors
mkdocs_git_authors_plugin/git/commit.py mkdocs_git_authors_plugin/git/command.py mkdocs_git_authors_plugin/plugin.py mkdocs_git_authors_plugin/util.py mkdocs_git_authors_plugin/git/page.py
mkdocs_git_authors_plugin/git/commit.py mkdocs_git_authors_plugin/git/command.py mkdocs_git_authors_plugin/plugin.py mkdocs_git_authors_plugin/util.py mkdocs_git_authors_plugin/git/page.py