git-archaeologist 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- git_archaeologist/__init__.py +0 -0
- git_archaeologist/analyzer.py +112 -0
- git_archaeologist/cli.py +243 -0
- git_archaeologist-1.0.0.dist-info/METADATA +63 -0
- git_archaeologist-1.0.0.dist-info/RECORD +8 -0
- git_archaeologist-1.0.0.dist-info/WHEEL +5 -0
- git_archaeologist-1.0.0.dist-info/entry_points.txt +2 -0
- git_archaeologist-1.0.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import git
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class RepositoryStats:
|
|
7
|
+
commits: int
|
|
8
|
+
contributors: set
|
|
9
|
+
files: int
|
|
10
|
+
insertions: int
|
|
11
|
+
deletions: int
|
|
12
|
+
lines: int
|
|
13
|
+
first_commit: object
|
|
14
|
+
latest_commit: object
|
|
15
|
+
lifespan: object
|
|
16
|
+
largest_gap: object
|
|
17
|
+
gap_start: object
|
|
18
|
+
gap_end: object
|
|
19
|
+
most_active_month: object
|
|
20
|
+
most_active_month_commits: int
|
|
21
|
+
history: list
|
|
22
|
+
|
|
23
|
+
def analyze_repo(path):
|
|
24
|
+
repo = git.Repo(path)
|
|
25
|
+
commits = list(repo.iter_commits("--all"))
|
|
26
|
+
|
|
27
|
+
if not commits:
|
|
28
|
+
return RepositoryStats(
|
|
29
|
+
commits=0,
|
|
30
|
+
contributors=set(),
|
|
31
|
+
files=0,
|
|
32
|
+
insertions=0,
|
|
33
|
+
deletions=0,
|
|
34
|
+
lines=0,
|
|
35
|
+
first_commit=None,
|
|
36
|
+
latest_commit=None,
|
|
37
|
+
lifespan=None,
|
|
38
|
+
largest_gap=None,
|
|
39
|
+
gap_start=None,
|
|
40
|
+
gap_end=None,
|
|
41
|
+
most_active_month=None,
|
|
42
|
+
most_active_month_commits=0
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
first_commit = min(commits, key=lambda commit: commit.committed_datetime)
|
|
46
|
+
latest_commit = max(commits, key=lambda commit: commit.committed_datetime)
|
|
47
|
+
lifespan = latest_commit.committed_datetime - first_commit.committed_datetime
|
|
48
|
+
sorted_commits = sorted(commits, key=lambda commit: commit.committed_datetime)
|
|
49
|
+
history = []
|
|
50
|
+
|
|
51
|
+
unique_contrib = set()
|
|
52
|
+
files = set()
|
|
53
|
+
total_insertions = 0
|
|
54
|
+
total_deletions = 0
|
|
55
|
+
total_lines = 0
|
|
56
|
+
monthly_commits = {}
|
|
57
|
+
for commit in commits:
|
|
58
|
+
unique_contrib.add(commit.author.name)
|
|
59
|
+
total_deletions += commit.stats.total['deletions']
|
|
60
|
+
total_insertions += commit.stats.total['insertions']
|
|
61
|
+
total_lines += commit.stats.total["lines"]
|
|
62
|
+
|
|
63
|
+
year = commit.committed_datetime.year
|
|
64
|
+
month = commit.committed_datetime.month
|
|
65
|
+
key = (year, month)
|
|
66
|
+
monthly_commits[key] = monthly_commits.get(key, 0) + 1
|
|
67
|
+
history.append((
|
|
68
|
+
commit.hexsha,
|
|
69
|
+
commit.committed_datetime,
|
|
70
|
+
commit.message,
|
|
71
|
+
commit.author
|
|
72
|
+
))
|
|
73
|
+
for file in commit.stats.files:
|
|
74
|
+
files.add(file)
|
|
75
|
+
number_files = len(files)
|
|
76
|
+
most_active_month = max(monthly_commits, key=monthly_commits.get)
|
|
77
|
+
|
|
78
|
+
largest_gap = timedelta(0)
|
|
79
|
+
gap_start = None
|
|
80
|
+
gap_end = None
|
|
81
|
+
|
|
82
|
+
for previous, current in zip(sorted_commits[:-1], sorted_commits[1:]):
|
|
83
|
+
gap = current.committed_datetime - previous.committed_datetime
|
|
84
|
+
if gap > largest_gap:
|
|
85
|
+
largest_gap = gap
|
|
86
|
+
gap_start = previous
|
|
87
|
+
gap_end = current
|
|
88
|
+
|
|
89
|
+
return RepositoryStats(
|
|
90
|
+
commits=len(commits),
|
|
91
|
+
contributors=unique_contrib,
|
|
92
|
+
files=number_files,
|
|
93
|
+
insertions=total_insertions,
|
|
94
|
+
deletions=total_deletions,
|
|
95
|
+
lines=total_lines,
|
|
96
|
+
first_commit=first_commit,
|
|
97
|
+
latest_commit=latest_commit,
|
|
98
|
+
lifespan=lifespan,
|
|
99
|
+
largest_gap=largest_gap,
|
|
100
|
+
gap_start=gap_start,
|
|
101
|
+
gap_end=gap_end,
|
|
102
|
+
most_active_month=most_active_month,
|
|
103
|
+
most_active_month_commits=monthly_commits[most_active_month],
|
|
104
|
+
history=history
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# CONGRATS YOU HAVE FOUND THE SECOND EASTER EGG!!!!
|
|
109
|
+
# KEEP GOING!!!!!!
|
|
110
|
+
# HOPE YOU LIKE THE PROGRAM
|
|
111
|
+
# MADE BY: OMAR GAMAL ELHALABY
|
|
112
|
+
# RATE IF YOU LIKE, OR LEAVE A COMMENT (IF FOUND ON HACKCLUB)!
|
git_archaeologist/cli.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import git
|
|
4
|
+
from git.exc import NoSuchPathError, InvalidGitRepositoryError
|
|
5
|
+
from git_archaeologist.analyzer import analyze_repo
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
def validator(path_str):
|
|
9
|
+
try:
|
|
10
|
+
path = Path(path_str)
|
|
11
|
+
git.Repo(path)
|
|
12
|
+
except NoSuchPathError:
|
|
13
|
+
raise argparse.ArgumentTypeError("ERROR: No such path")
|
|
14
|
+
except InvalidGitRepositoryError:
|
|
15
|
+
raise argparse.ArgumentTypeError("ERROR: git repository not found")
|
|
16
|
+
return path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def print_header():
|
|
20
|
+
print("Git Archaeologist")
|
|
21
|
+
print("===========================\n")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def print_timeline_section(stats):
|
|
25
|
+
print("REPOSITORY TIMELINE")
|
|
26
|
+
print("---------------------------")
|
|
27
|
+
print(f"First Commit: {stats.first_commit.committed_datetime}")
|
|
28
|
+
print(f"Last Commit: {stats.latest_commit.committed_datetime}")
|
|
29
|
+
print(f"Lifespan: {stats.lifespan}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def print_timeline(stats):
|
|
33
|
+
print_header()
|
|
34
|
+
print_timeline_section(stats)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def print_activity_section(stats):
|
|
38
|
+
print("REPOSITORY ACTIVITY")
|
|
39
|
+
print("---------------------------")
|
|
40
|
+
print(
|
|
41
|
+
f"Most active month: "
|
|
42
|
+
f"{stats.most_active_month[1]}/{stats.most_active_month[0]}"
|
|
43
|
+
)
|
|
44
|
+
print(
|
|
45
|
+
f"Commits in the most active month: "
|
|
46
|
+
f"{stats.most_active_month_commits}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if stats.largest_gap:
|
|
50
|
+
print(f"Longest inactivity: {stats.largest_gap}")
|
|
51
|
+
else:
|
|
52
|
+
print("Longest inactivity: 0 days")
|
|
53
|
+
|
|
54
|
+
if stats.gap_start is None:
|
|
55
|
+
print("From: No Gap!")
|
|
56
|
+
else:
|
|
57
|
+
print(f"From: {stats.gap_start.committed_datetime}")
|
|
58
|
+
|
|
59
|
+
if stats.gap_end is None:
|
|
60
|
+
print("To: No Gap!")
|
|
61
|
+
else:
|
|
62
|
+
print(f"To: {stats.gap_end.committed_datetime}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def print_activity(stats):
|
|
66
|
+
print_header()
|
|
67
|
+
print_activity_section(stats)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def print_summary_section(stats):
|
|
71
|
+
print("REPOSITORY SUMMARY")
|
|
72
|
+
print("---------------------------")
|
|
73
|
+
print(f"Commits: {stats.commits}")
|
|
74
|
+
print(f"Contributors: {stats.contributors}")
|
|
75
|
+
print(f"Files: {stats.files}")
|
|
76
|
+
print(f"Insertions: {stats.insertions}")
|
|
77
|
+
print(f"Deletions: {stats.deletions}")
|
|
78
|
+
print(f"Lines Changed: {stats.lines}\n")
|
|
79
|
+
|
|
80
|
+
print("CONTRIBUTORS")
|
|
81
|
+
print("---------------------------")
|
|
82
|
+
|
|
83
|
+
for name in stats.contributors:
|
|
84
|
+
print(name)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def print_summary(stats):
|
|
88
|
+
print_header()
|
|
89
|
+
print_summary_section(stats)
|
|
90
|
+
|
|
91
|
+
def print_history(stats):
|
|
92
|
+
print_header()
|
|
93
|
+
print("COMMIT HISTORY")
|
|
94
|
+
print("---------------------------")
|
|
95
|
+
commit_num = 0
|
|
96
|
+
|
|
97
|
+
for commit in stats.history:
|
|
98
|
+
commit_num += 1
|
|
99
|
+
print(f"Commit number: {commit_num}")
|
|
100
|
+
print(f"Commit SHA1 (first 7 digits): {stats.history[commit_num - 1][0][:7]}")
|
|
101
|
+
print(f"Commit Date/Time: {stats.history[commit_num - 1][1]}")
|
|
102
|
+
print(f"Commit message: {stats.history[commit_num - 1][2]}")
|
|
103
|
+
print(f"Commit author(s): {stats.history[commit_num - 1][3]}\n")
|
|
104
|
+
|
|
105
|
+
def print_json(stats):
|
|
106
|
+
dict_stats = vars(stats)
|
|
107
|
+
dict_stats["contributors"] = list(stats.contributors)
|
|
108
|
+
dict_stats["largest_gap"] = str(stats.largest_gap)
|
|
109
|
+
dict_stats["gap_start"] =(
|
|
110
|
+
str(stats.gap_start.committed_datetime)
|
|
111
|
+
if stats.gap_start else None
|
|
112
|
+
)
|
|
113
|
+
dict_stats["gap_end"] = (
|
|
114
|
+
str(stats.gap_end.committed_datetime)
|
|
115
|
+
if stats.gap_end else None
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
dict_stats["first_commit"] = (
|
|
119
|
+
str(stats.first_commit.committed_datetime)
|
|
120
|
+
if stats.first_commit else None
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
dict_stats["latest_commit"] = (
|
|
124
|
+
str(stats.latest_commit.committed_datetime)
|
|
125
|
+
if stats.latest_commit else None
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
dict_stats["lifespan"] = str(stats.lifespan)
|
|
129
|
+
history = []
|
|
130
|
+
commit_number = 0
|
|
131
|
+
for commit_hash, date, message, author in stats.history:
|
|
132
|
+
commit_number += 1
|
|
133
|
+
history.append({
|
|
134
|
+
"number": (commit_number),
|
|
135
|
+
"commit SHA1": commit_hash,
|
|
136
|
+
"date":str(date),
|
|
137
|
+
"message": message.strip(),
|
|
138
|
+
"author(s)": str(author)
|
|
139
|
+
})
|
|
140
|
+
dict_stats["history"] = history
|
|
141
|
+
print(json.dumps(dict_stats, indent=4))
|
|
142
|
+
|
|
143
|
+
def print_all(stats):
|
|
144
|
+
print_header()
|
|
145
|
+
|
|
146
|
+
print("REPOSITORY OVERVIEW")
|
|
147
|
+
print("---------------------------")
|
|
148
|
+
print(f"Commits: {stats.commits}")
|
|
149
|
+
print(f"Contributors: {len(stats.contributors)}")
|
|
150
|
+
print(f"Num. of Files: {stats.files}")
|
|
151
|
+
print(f"Insertions: {stats.insertions}")
|
|
152
|
+
print(f"Deletions: {stats.deletions}")
|
|
153
|
+
print(f"Lines Changed: {stats.lines}\n")
|
|
154
|
+
|
|
155
|
+
print_timeline_section(stats)
|
|
156
|
+
print()
|
|
157
|
+
print_activity_section(stats)
|
|
158
|
+
print()
|
|
159
|
+
print_summary_section(stats)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main():
|
|
163
|
+
parser = argparse.ArgumentParser(
|
|
164
|
+
description="A git tool to view information regarding repositories"
|
|
165
|
+
)
|
|
166
|
+
output_group = parser.add_mutually_exclusive_group()
|
|
167
|
+
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
"repository",
|
|
170
|
+
type=validator,
|
|
171
|
+
help="Path to repository"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"-v",
|
|
176
|
+
"--version",
|
|
177
|
+
action="version",
|
|
178
|
+
version="1.0.0",
|
|
179
|
+
help="show the program's version",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
output_group.add_argument(
|
|
183
|
+
"-s",
|
|
184
|
+
"--summary",
|
|
185
|
+
action="store_true",
|
|
186
|
+
help="show a simple summary"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
output_group.add_argument(
|
|
190
|
+
"-t",
|
|
191
|
+
"--timeline",
|
|
192
|
+
action="store_true",
|
|
193
|
+
help="show a simple timeline for the repository"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
output_group.add_argument(
|
|
197
|
+
"-a",
|
|
198
|
+
"--activity",
|
|
199
|
+
action="store_true",
|
|
200
|
+
help="show a simple activity dashboard for the repository"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
output_group.add_argument(
|
|
204
|
+
"--history",
|
|
205
|
+
action="store_true",
|
|
206
|
+
help="show the history of the repository"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
output_group.add_argument(
|
|
210
|
+
"-j",
|
|
211
|
+
"--json",
|
|
212
|
+
action="store_true",
|
|
213
|
+
help="output repository statistics as JSON"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
parsed_args = parser.parse_args()
|
|
217
|
+
stats = analyze_repo(parsed_args.repository)
|
|
218
|
+
if stats.commits == 0:
|
|
219
|
+
print_header()
|
|
220
|
+
print("ERROR: Repository has no commits.")
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
if parsed_args.timeline:
|
|
224
|
+
print_timeline(stats)
|
|
225
|
+
elif parsed_args.activity:
|
|
226
|
+
print_activity(stats)
|
|
227
|
+
elif parsed_args.summary:
|
|
228
|
+
print_summary(stats)
|
|
229
|
+
elif parsed_args.history:
|
|
230
|
+
print_history(stats)
|
|
231
|
+
elif parsed_args.json:
|
|
232
|
+
print_json(stats)
|
|
233
|
+
else:
|
|
234
|
+
print_all(stats)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
main()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# HOPE YOU'RE HAPPY WITH THE TOOL!!!!!!!
|
|
242
|
+
# MADE BY: OMAR ELHALABY
|
|
243
|
+
# CONGRATS ON FINDING THE THIRD EASYER EGG!!
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: git-archaeologist
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A CLI tool for analyzing Git repository history
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: GitPython
|
|
8
|
+
|
|
9
|
+
# Git Archaeologist
|
|
10
|
+
A python CLI tool for quickly analyzing and exploring the history of Git repositories.
|
|
11
|
+
|
|
12
|
+
# Problem
|
|
13
|
+
Understanding a Git repository's history can be very time consuming when done manually. Git provides a huge amount of information, but finding useful patterns-such as the repository's lifespan, periods of inactivity, most active months, contributors, and commit history-often requires running multiple commands and manually interpreting the results.
|
|
14
|
+
|
|
15
|
+
__Git Archaeologist__ simplifies this process by collecting important repository statistics and presenting them in a clear human-readable format or a JSON format for developers.
|
|
16
|
+
|
|
17
|
+
# Features
|
|
18
|
+
|
|
19
|
+
- Repository summary
|
|
20
|
+
- Total commits
|
|
21
|
+
- Number of contributors
|
|
22
|
+
- Number of lines changed
|
|
23
|
+
- Total insertions
|
|
24
|
+
- Total deletions
|
|
25
|
+
- Total lines changed
|
|
26
|
+
|
|
27
|
+
- Timeline analysis
|
|
28
|
+
- First commit
|
|
29
|
+
- Latest commit
|
|
30
|
+
- Repo Lifespan
|
|
31
|
+
|
|
32
|
+
- Activity analysis
|
|
33
|
+
- Most active month
|
|
34
|
+
- Number of commits during the most active month
|
|
35
|
+
- Longest period of inactivity
|
|
36
|
+
- Commits marking the begining and end of the longest gap
|
|
37
|
+
|
|
38
|
+
- Commit History
|
|
39
|
+
- Commit SHA1
|
|
40
|
+
- Commit data
|
|
41
|
+
- Commit message
|
|
42
|
+
- Author
|
|
43
|
+
|
|
44
|
+
- JSON output
|
|
45
|
+
- Machine-readable repository statistics
|
|
46
|
+
- Useful for scripts, automation and other apps
|
|
47
|
+
|
|
48
|
+
# Installation
|
|
49
|
+
|
|
50
|
+
Clone the repository:
|
|
51
|
+
|
|
52
|
+
``` bash
|
|
53
|
+
$ git clone https://github.com/omarelhalaby76-coder/git-archaeologist
|
|
54
|
+
$ cd git-archaeologist
|
|
55
|
+
```
|
|
56
|
+
Install the dependencies:
|
|
57
|
+
``` bash
|
|
58
|
+
$ pip install -r requirments.txt
|
|
59
|
+
```
|
|
60
|
+
Or install the project directly:
|
|
61
|
+
``` bash
|
|
62
|
+
$ pip install .
|
|
63
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
git_archaeologist/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
git_archaeologist/analyzer.py,sha256=mxQLEgwnvIUA9FFzBO0wMAn5EAW1Y7rf6YDsBUOKnaQ,3860
|
|
3
|
+
git_archaeologist/cli.py,sha256=_gSGNu4P2L8kGfejFVJRHzu0eQID7Kk_1YyVIysRcTI,6480
|
|
4
|
+
git_archaeologist-1.0.0.dist-info/METADATA,sha256=LlaGaDhhPY71-xmFkcmd5RBQnt34l4N5Mltv7M1ldqQ,1813
|
|
5
|
+
git_archaeologist-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
+
git_archaeologist-1.0.0.dist-info/entry_points.txt,sha256=CCaVx5vkVbWYlgHoMmstVpuYDxnESzBshhKuHkOJYac,65
|
|
7
|
+
git_archaeologist-1.0.0.dist-info/top_level.txt,sha256=dUOvLs7-v4cBBpzSJghnkHJWms7Ofwf3Zn__Rn-p07o,18
|
|
8
|
+
git_archaeologist-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
git_archaeologist
|