Browse Source

Initial commit

First version of commit iterators & office_hours tester
Yann Weber 7 months ago
commit
1f02bacca6
4 changed files with 237 additions and 0 deletions
  1. 12
    0
      Makefile
  2. 3
    0
      NOTES
  3. 86
    0
      git_oh.py
  4. 136
    0
      test.py

+ 12
- 0
Makefile View File

@@ -0,0 +1,12 @@
1
+all: unittest lint mypy
2
+
3
+unittest:
4
+	python3 -m unittest -v
5
+
6
+lint:
7
+	pylint git_oh.py test.py
8
+
9
+mypy:
10
+	mypy git_oh.py
11
+
12
+.PHONY: lint mypy

+ 3
- 0
NOTES View File

@@ -0,0 +1,3 @@
1
+- creating commit at arbitrary dates :
2
+ - git commit --date option change the authored date
3
+ - GIT_COMMITTER_DATE env variable change the committed datetime

+ 86
- 0
git_oh.py View File

@@ -0,0 +1,86 @@
1
+#!/usr/bin/python3
2
+""" Entrouvert test """
3
+import datetime
4
+import shutil
5
+import tempfile
6
+import warnings
7
+from collections.abc import Sequence
8
+from typing import Generator
9
+
10
+
11
+import git
12
+
13
+from git.repo.base import Repo
14
+from git.objects.commit import Commit
15
+
16
+
17
+def in_office_hours(moment:datetime.datetime=datetime.datetime.now(),
18
+        starthour:datetime.time=datetime.time(8,0,0),
19
+        stophour:datetime.time=datetime.time(20,0,0),
20
+        weekend:Sequence[int]=(5,6))->bool:
21
+    """ Indicates if a moment is in office hours.
22
+
23
+    Office hours are localized, comparisons are done without
24
+    taking care of tzoffset : in fact, if a working day starts at
25
+    08:00, 07:59:59+0200 is off as 07:59:59+0000 is.
26
+
27
+    Arguments :
28
+        - moment : the moment to compare with office hours
29
+        - starthour : standard day start hour
30
+        - stophour : standard day stop hour
31
+        - weekend : list of days off (0,1,....,6)
32
+    """
33
+    for dow in weekend:
34
+        if dow < 0 or dow > 6:
35
+            raise ValueError("Weekend days are integer in [0..6]")
36
+
37
+    if starthour.tzinfo is not None or stophour.tzinfo is not None:
38
+        warnings.warn("Start & stop hours should not indicate any \
39
+tzinfo : comparisons are done without taking tzoffset in considaration")
40
+
41
+    if moment.weekday() in weekend:
42
+        return False
43
+    localtime = moment.time()
44
+    return starthour <= localtime <= stophour
45
+
46
+
47
+def iter_commits(repo:Repo)->Generator[Commit, None, None]:
48
+    """ Generator on all git commits in given repository.
49
+
50
+    Recursively iterate on each commit in each branch/ref not yielding
51
+    duplicates commits (based on commit hashes)
52
+
53
+    Arguments :
54
+        - repo : The repository instance to fetch commits from
55
+    """
56
+    encountered = set()
57
+    for ref in repo.refs:
58
+        for commit in repo.iter_commits(ref.name):
59
+            if commit.binsha not in encountered:
60
+                encountered.add(commit.binsha)
61
+                yield commit
62
+
63
+
64
+class TempRemoteRepo(Repo):
65
+    """ A temporary repository referencing a remote repository
66
+
67
+    Allows to iterate on all commits without cloning the remote
68
+    """
69
+
70
+    def __init__(self, remote_url:str):
71
+        """ Initialize a new empty repository referencing a remote repo
72
+
73
+        Arguments :
74
+            - remote_url : The url of the remote repo to reference
75
+        """
76
+        self.temppath = tempfile.mkdtemp(prefix="git_oh_")
77
+        git.Repo.init(self.temppath)
78
+        super().__init__(self.temppath)
79
+        self.create_remote("origin", remote_url).fetch()
80
+
81
+    def __del__(self):
82
+        shutil.rmtree(self.temppath)
83
+        super().__del__()
84
+
85
+    def __iter__(self)->Generator[Commit, None, None]:
86
+        return iter_commits(self)

+ 136
- 0
test.py View File

@@ -0,0 +1,136 @@
1
+#!/usr/bin/env python3
2
+""" Tests for git_oh script """
3
+
4
+import datetime
5
+import gc
6
+import os
7
+import random
8
+import shutil
9
+import tempfile
10
+import unittest
11
+
12
+import git
13
+
14
+import git_oh
15
+
16
+
17
+class TestGitIterations(unittest.TestCase):
18
+    """ Testing iterator on git commits """
19
+
20
+    def setUp(self):
21
+        self.repo_path = tempfile.mkdtemp(prefix="git_oh_test_")
22
+        self.repo = git.Repo.init(self.repo_path)
23
+        self.actors = [git.Actor(f"User{i:d}", f"user{i:d}@localhost")
24
+                       for i in range(8)]
25
+
26
+    def tearDown(self):
27
+        shutil.rmtree(self.repo_path)
28
+
29
+    def test_metadata(self):
30
+        """ Fetch commits and their metadata in a simple git repository """
31
+        fromisoformat = datetime.datetime.fromisoformat
32
+        commit_date_fmt = "2022-12-13T23:32:2%d+0200"
33
+
34
+        commits = [
35
+                self.git_commit_mod(author=self.actors[0],
36
+                    commit_date=fromisoformat(commit_date_fmt % i))
37
+                for i in range(10)]
38
+        found_commits = list(git_oh.iter_commits(self.repo))
39
+
40
+        set_a = {c.hexsha for c in commits}
41
+        set_b = {c.hexsha for c in found_commits}
42
+
43
+        self.assertEqual(set_a, set_b)
44
+
45
+        for commit in found_commits:
46
+            self.assertEqual(commit.author, self.actors[0])
47
+            self.assertLessEqual(commit.committed_datetime,
48
+                    fromisoformat(commit_date_fmt % 9))
49
+            self.assertGreaterEqual(commit.committed_datetime,
50
+                    fromisoformat(commit_date_fmt % 0))
51
+
52
+    def test_branches(self):
53
+        """ Fetch commits from a repo with mutliple branches """
54
+
55
+        commits = [self.git_commit_mod(author=self.actors[0])
56
+                for _ in range(10)]
57
+
58
+        self.repo.git.checkout("HEAD", b="new_branch")
59
+
60
+        commits += [self.git_commit_mod(author=self.actors[0])
61
+                for _ in range(10)]
62
+
63
+        self.repo.git.checkout("HEAD", b="another_branch")
64
+
65
+        commits += [self.git_commit_mod(author=self.actors[0])
66
+                for _ in range(10)]
67
+
68
+        self.repo.git.checkout("new_branch")
69
+
70
+        found_commits = {commit.hexsha
71
+            for commit in git_oh.iter_commits(self.repo)}
72
+        commits = {commit.hexsha for commit in commits}
73
+
74
+        self.assertEqual(commits, found_commits)
75
+
76
+
77
+    def test_remote(self):
78
+        """ Testing TempRemoteRepo class commit fetch __iter__ method """
79
+        commits = {self.git_commit_mod(author=self.actors[0]).hexsha
80
+                for _ in range(10)}
81
+        repo = git_oh.TempRemoteRepo(f"file://{self.repo_path:s}")
82
+
83
+        found_commits = {commit.hexsha for commit in repo}
84
+
85
+        self.assertEqual(commits, found_commits)
86
+
87
+
88
+    def test_remote_cleanup(self):
89
+        """ Testing TempRemoteRepo class cleanup """
90
+        repo = git_oh.TempRemoteRepo(f"file://{self.repo_path:s}")
91
+        tmppath = repo.temppath
92
+
93
+        _ = list(repo)
94
+
95
+        self.assertTrue(os.path.isdir(tmppath))
96
+        del repo
97
+        gc.collect() # asking gc to call repo.__del__()
98
+        self.assertFalse(os.path.isdir(tmppath))
99
+
100
+
101
+    def test_remote_branches(self):
102
+        """ Testing TempRemoteRepo branch commit fetch """
103
+
104
+        commits = []
105
+        for i in range(5):
106
+            commits += [self.git_commit_mod(author=self.actors[0])
107
+                    for _ in range(10)]
108
+            self.repo.git.checkout("HEAD", b=f"branch-{i:d}")
109
+
110
+        repo = git_oh.TempRemoteRepo(f"file://{self.repo_path:s}")
111
+
112
+        found_commits = {commit.hexsha for commit in repo}
113
+        commits = {commit.hexsha for commit in commits}
114
+
115
+        self.assertEqual(commits, found_commits)
116
+
117
+
118
+    def git_commit_mod(self, msg="new commit", filename="foo.txt",
119
+            **commit_kwargs):
120
+        """ Add a random modification to repository and commit them
121
+            Arguments :
122
+                - msg : commit message
123
+                - filename : the filename to modify/add/commit
124
+                - **commit_kwargs : see help(git.index.base.IndexFile.commit)
125
+            Returns :
126
+                - the commit instance
127
+        """
128
+        with open(os.path.join(self.repo_path, filename), "w+",
129
+                encoding="utf-8") as repo_fp:
130
+            repo_fp.write(random.choices("abcdef")[0])
131
+        self.repo.index.add([filename])
132
+        return self.repo.index.commit(msg, **commit_kwargs)
133
+
134
+
135
+if __name__ == "__main__":
136
+    unittest.main()

Loading…
Cancel
Save