sgw/sgw.py

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}">&#9664; all repos</a> <br /> 39 <a href="/{WEB_DIR}/{repo_name}">index</a> &VerticalSeparator; 40 <a href="/{WEB_DIR}/{repo_name}/files.html">files</a> &VerticalSeparator; 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)