1#!/usr/bin/env python3
2"""
3sgw - Static Git Webpage generator
4
5Depends: the git binary to be in your $PATH
6Thanks: https://ratfactor.com/repos/reporat/
7"""
8
9from datetime import datetime
10from pathlib import Path
11import subprocess
12import sys
13import html as h
14import shutil
15
16DOMAIN = "https://www.aktsbot.in/git-web"
17WEB_DIR = "git-web"
18OUTPUT_PATH = "/tmp/sgw/" + WEB_DIR
19
20css = """
21*,*::before,*::after{box-sizing:border-box}html{font-family:sans-serif;font-size:11pt;line-height:1.45em}body{color:#555;background-color:#f2f2f2}nav a{text-decoration:none}a{color:#4472c1}.container{max-width:1050px;margin:0 auto}.right{text-align:right}.files ul{font-family:monospace;list-style:none;padding:0;padding:5px 0}.bt{border-top:1px solid lightgrey}.bb{border-bottom:1px solid lightgrey}.readme{overflow-x:auto}footer div{border-top:1px solid lightgrey;margin:5px 0}.source-file{font-family:monospace;white-space:pre-wrap}.source-file a{margin-right:1em;text-decoration:none;user-select:none}@media (prefers-color-scheme:dark){body{color:#bfbfbf;background-color:#1a1a1a}}
22"""
23
24def header(title, repo_name):
25 return f"""
26<!DOCTYPE html>
27<html lang="en">
28<head>
29 <meta charset="UTF-8">
30 <meta name="viewport" content="width=device-width, initial-scale=1.0">
31 <title>{title}</title>
32 <style>{css}</style>
33</head>
34<body>
35 <header>
36 <div class="container">
37 <nav>
38 <a href="/{WEB_DIR}">◀ all repos</a> <br />
39 <a href="/{WEB_DIR}/{repo_name}">index</a> ❘
40 <a href="/{WEB_DIR}/{repo_name}/files.html">files</a> ❘
41 <a href="/{WEB_DIR}/{repo_name}/commits.html">commits</a>
42 </nav>
43 </div>
44 </header>"""
45
46def footer():
47 ts = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")
48 return f"""
49 <footer>
50 <div class="container right">
51 <small>sgw | {ts}</small>
52 </div>
53 </footer>
54</body>
55</html>"""
56
57def is_plain_text(file_path):
58 try:
59 with open(file_path, 'tr') as f:
60 # Try to read a small portion of the file in text mode
61 f.read(1024)
62 return True
63 except UnicodeDecodeError:
64 # If it throws a decoding error, it is a binary file
65 return False
66 except IOError:
67 print("Could not open or read the file.")
68 return False
69
70def get_repo_description():
71 file_path = Path(".git/description")
72 if file_path.is_file():
73 with open(file_path, "r") as file:
74 return file.read().strip()
75 else:
76 return "No description"
77
78def write_file(file_path, file_content):
79 with open(file_path, "w") as file:
80 file.write(file_content)
81
82def make_file_content_html(file_path, repo_name):
83 html = f"""
84 <section class="container">
85 <h3>{repo_name}/{file_path}</h3>
86 <div class="source-file">"""
87
88 with open(file_path, "r") as file:
89 for line_number, line in enumerate(file, start=1):
90 # line.strip() removes the trailing newline character (\n)
91 safe_line = h.escape(line)
92 html += f"""<a id=\"L{line_number}\" href=\"#L{line_number}\">{line_number}</a>{safe_line}"""
93
94 html += f"""
95 </div>
96 </section>"""
97
98 return html
99
100def make_files_html(html_files, repo_path, file_count, repo_name):
101 html = header(title=repo_name +" files", repo_name=repo_name)
102 html += f"""<section class="container">
103 <h3>{file_count} files</h3>
104 <div class="files bt">
105 <ul>"""
106 for f in html_files:
107 # <li><a href="file.html">app.js</a></li>
108 html += f"<li><a href=\"{f.get('url')}\">{f.get('fname')}</a></li>"
109
110 html += """</ul>
111 </div>
112 </section>"""
113
114 html += footer()
115 write_file(file_path=repo_path + "/files.html", file_content=html)
116
117def make_commits_html(git_commits, repo_path, repo_name, git_branch):
118 html = header(title=repo_name +" commit history", repo_name=repo_name)
119 html += f"""<section class="container">
120 <h3>{repo_name} commit history - {git_branch}</h3>
121 <div class="source-file">{git_commits}\n</div>
122 </section>"""
123
124 html += footer()
125 write_file(file_path=repo_path + "/commits.html", file_content=html)
126
127def make_index_html(html_files, read_me, repo_name, repo_description, repo_path):
128 html = header(title=repo_name+" - " +repo_description, repo_name=repo_name)
129 html += f"""<section class="container">
130 <div>
131 <h1>{repo_name}</h1>
132 <p>{repo_description}</p>
133 <p><code>git clone {DOMAIN}/{repo_name}/{repo_name}.git</code></p>
134 </div>
135
136 <div class="files bb bt">
137 <ul>"""
138 for f in html_files:
139 # <li><a href="file.html">app.js</a></li>
140 html += f"<li><a href=\"{f.get('url')}\">{f.get('fname')}</a></li>"
141
142 html += """</ul>
143 </div>"""
144
145 html += f"""<div class="readme">
146 <pre><code>{read_me}</code></pre>
147 </div>
148 </section>
149 """
150
151 html += footer()
152 write_file(file_path=repo_path + "/index.html", file_content=html)
153
154def get_file_content(file_path):
155 content = Path(file_path).read_text()
156 return content
157
158def current_folder():
159 folder = Path.cwd().name
160 return folder
161
162def setup_dest(repo_name):
163 repo_path = OUTPUT_PATH + "/" + repo_name
164 repo_files_path = repo_path + "/files"
165 r_path = Path(repo_files_path)
166 r_path.mkdir(parents=True, exist_ok=True)
167
168 bare_path = OUTPUT_PATH + "/" + repo_name + "/" + repo_name + ".git"
169 b_path = Path(bare_path)
170 if not b_path.is_dir():
171 # no project.git exists, make it
172 ps_bare_repo = subprocess.run(["git", "clone", "--bare", ".", bare_path])
173 if ps_bare_repo.returncode != 0:
174 print(f"error: couldnt setup bare repo in {bare_path}")
175 sys.exit(1)
176
177 return repo_path, repo_files_path , bare_path
178
179def process_git_files(git_files, repo_files_path, repo_path, repo_name, repo_description):
180 file_count = 0
181 html_files = [] # list
182 read_me = ''
183 # will hold items that look like <li><a href="file.html">app.js</a></li>
184
185 for file_path in git_files.splitlines():
186 file_count += 1
187 fp = Path(file_path) # code/foo/bar/foo.txt
188 fname = fp.name # foo.txt
189 fparent = fp.parent # code/foo/bar
190
191 dest_path = repo_files_path
192 if str(fparent) != '.':
193 print('d: ' + str(fparent))
194 dest_path += "/" + str(fparent)
195 repo_folder_path = Path(dest_path)
196 repo_folder_path.mkdir(parents=True, exist_ok=True)
197
198
199 print('f: ' + str(fname))
200 if is_plain_text(file_path=file_path):
201 print(' plain-text - ' + file_path)
202 # start of making dest file
203 html = header(title=repo_name+"/"+file_path, repo_name=repo_name)
204 html += make_file_content_html(file_path=file_path, repo_name=repo_name)
205 html += footer()
206 write_file(file_path=dest_path + "/" + fp.name + ".html", file_content=html)
207
208 html_files.append({"fname": file_path, "url": "files/" + file_path + ".html"})
209
210 # readme.md? in project root
211 if file_path in ('README.md', 'README.txt', 'README') :
212 read_me = get_file_content(file_path=file_path)
213 else:
214 print(' blob - ' + file_path)
215 html_files.append({"fname": file_path, "url": "files/" + file_path})
216 shutil.copy(file_path, dest_path+"/"+fp.name)
217
218 # files.html
219 make_files_html(html_files=html_files, repo_path=repo_path, file_count=file_count, repo_name=repo_name)
220
221 # index.html
222 make_index_html(html_files=html_files, read_me=read_me, repo_name=repo_name, repo_description=repo_description, repo_path=repo_path)
223
224 return
225
226def main():
227 ps_branch = subprocess.run(['git', 'branch', '--show-current'], capture_output=True, text=True)
228 if ps_branch.returncode != 0:
229 print("error: are we in a git repo?")
230 sys.exit(1)
231 git_branch = ps_branch.stdout
232
233 ps_commits = subprocess.run(['git', 'log'], capture_output=True, text=True)
234 if ps_commits.returncode != 0:
235 print("error: what the! can't get commit history?")
236 sys.exit(1)
237 git_commits = ps_commits.stdout
238
239 repo_name = current_folder()
240
241 # .git/description contents
242 repo_description = get_repo_description()
243 print(f"sgw.py is generating site for")
244 print(" " + repo_name)
245 print(" " + repo_description)
246 print("")
247
248 # getting the files of the repo
249 ps_files = subprocess.run(["git", "ls-tree", "-r", "--name-only", "HEAD"], capture_output=True, text=True)
250 if ps_files.returncode != 0:
251 print("error: no files found in repo. are we in a git repo with files?")
252 sys.exit(1)
253 git_files = ps_files.stdout
254
255 repo_path, repo_files_path, bare_path = setup_dest(repo_name=repo_name)
256
257 process_git_files(git_files=git_files, repo_files_path=repo_files_path, repo_path=repo_path, repo_name=repo_name, repo_description=repo_description)
258
259 make_commits_html(git_commits=git_commits, repo_name=repo_name, repo_path=repo_path, git_branch=git_branch)
260
261 # at the very last, we sync and update the output's bare repo for "dumb http" git cloning.
262 cwd = Path.cwd()
263 sync_command = f"""cd {bare_path} && git fetch {cwd} '*:*' && git update-server-info"""
264 subprocess.run(sync_command, shell=True)
265
266 print("")
267 print("site has been generated")
268 print(f"output: {repo_path}")
269
270# entry
271if __name__ == "__main__":
272 try:
273 main()
274 except KeyboardInterrupt:
275 sys.exit(0)