ctucx.git: nimstagit

[nimlang] incomplete reimplementation of stagit

commit cb0e968e2672912f853cd35be9fe8441a4c6d00b
Author: Leah (ctucx) <leah@ctu.cx>
Date: Sun, 21 Mar 2021 22:56:41 +0100

init
16 files changed, 1233 insertions(+), 0 deletions(-)
A
.gitignore
|
5
+++++
A
nimstagit.nimble
|
14
++++++++++++++
A
src/assets.nim
|
9
+++++++++
A
src/assets/overview.html
|
50
++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/assets/repoBlob.html
|
42
++++++++++++++++++++++++++++++++++++++++++
A
src/assets/repoCommit.html
|
41
+++++++++++++++++++++++++++++++++++++++++
A
src/assets/repoLog.html
|
56
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/assets/repoRefs.html
|
29
+++++++++++++++++++++++++++++
A
src/assets/repoSummary.html
|
77
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/assets/repoTree.html
|
60
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/assets/style.css
|
283
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/index.nim
|
67
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/nimstagit.nim
|
59
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/repoGenerator.nim
|
352
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/types.nim
|
10
++++++++++
A
src/utils.nim
|
79
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,5 @@
+
+libgit2.so
+nimstagit
+out/
+test.conf
diff --git a/nimstagit.nimble b/nimstagit.nimble
@@ -0,0 +1,14 @@
+# Package
+version       = "0.1.0"
+author        = "Leah (ctucx)"
+description   = "stagit reimplementation in nimlang"
+license       = "GPL-3.0"
+srcDir        = "src"
+bin           = @["nimstagit"]
+
+
+# Dependencies
+requires "nim >= 1.4.4"
+requires "moustachu"
+requires "markdown"
+requires "https://cgit.ctu.cx/nimgit"
diff --git a/src/assets.nim b/src/assets.nim
@@ -0,0 +1,9 @@
+const
+  assetStyleCss*       = staticRead "assets/style.css" 
+  templateOverview*    = staticRead "assets/overview.html"
+  templateRepoSummary* = staticRead "assets/repoSummary.html"
+  templateRepoLog*     = staticRead "assets/repoLog.html"
+  templateRepoCommit*  = staticRead "assets/repoCommit.html"
+  templateRepoTree*    = staticRead "assets/repoTree.html"
+  templateRepoBlob*    = staticRead "assets/repoBlob.html"
+  templateRepoRefs*    = staticRead "assets/repoRefs.html"
diff --git a/src/assets/overview.html b/src/assets/overview.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>{{repoName}} - {{siteTitle}}</title>
+
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0">
+		<meta name="description" content="{{description}}">
+        <meta name="generator" content="https://cgit.ctu.cx/nimstagit">
+
+		<link href="/style.css" rel="stylesheet" />
+	</head>
+	<body>
+		<header>
+			<h1>{{siteTitle}}</h1>
+			<p>{{description}}</p>
+		</header>
+		<nav>
+            <a class="active" href="index.html">index</a>
+		</nav>
+		<main>
+			<h3>Overview</h3>
+            <table>
+                <thead>
+                    <tr>
+                        <td>Name</td>
+                        <td>Description</td>
+                        <td>Idle</td>
+                    </tr>
+                </thead>
+                {{#categories}}
+                {{#name}}
+				<tr>
+					<td colspan="4" class="reposection">{{name}}</td>
+				</tr>
+				{{/name}}
+				{{#repos}}
+                <tr>
+                    <td class="sublevel-repo"><a href="{{repoUrl}}/">{{repoName}}</a></td>
+                    <td><a href="{{repoUrl}}/">{{description}}</a></td>
+                    <td><span class="age-hours"><a href="/{{repoUrl}}/log/{{objId}}.html">{{lastActivity}}</a></span></td>
+                </tr>
+                {{/repos}}
+				{{/categories}}
+            </table>
+		</main>
+		<footer>Generated on {{generated}}</footer>
+	</body>
+</html>
+
diff --git a/src/assets/repoBlob.html b/src/assets/repoBlob.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>{{repoName}} - {{siteTitle}}</title>
+
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0">
+		<meta name="description" content="{{description}}">
+        <meta name="generator" content="https://cgit.ctu.cx/nimstagit">
+
+		<link href="/style.css" rel="stylesheet" />
+	</head>
+	<body>
+		<header>
+			<h1><a href="/">{{siteTitle}}</a>: {{repoName}}</h1>
+			<p>{{description}}</p>
+		</header>
+		<nav>
+            <a href="/{{repoUrl}}">summary</a>
+			<a href="/{{repoUrl}}/log">log</a>
+			<a href="/{{repoUrl}}/tree" class="active">tree</a>
+			<a href="/{{repoUrl}}/refs">refs</a>
+		</nav>
+		<main>
+			{{#path}}<a href="{{url}}">{{name}}</a> / {{/path}} {{filename}} (<a href="{{filename}}">plain</a>)<br>
+            blob: {{id}} {{filesize}}
+            {{#isBinary}}
+            <pre>Binary file.</pre>
+            {{/isBinary}}
+            {{^isBinary}}
+            <div class="code">
+	            <pre class="lines">{{#lines}}<a id="L{{.}}" href="#L{{.}}">{{.}}</a>
+{{/lines}}</pre>
+	            <pre class="highlight">{{content}}</pre>
+	        </div>
+            {{/isBinary}}
+		</main>
+		<footer>Generated on {{generated}}</footer>
+		<script src="https://git.sr.ht/static/linelight.js"></script>
+	</body>
+</html>
+
diff --git a/src/assets/repoCommit.html b/src/assets/repoCommit.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>{{repoName}} - {{siteTitle}}</title>
+
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0">
+		<meta name="description" content="{{description}}">
+        <meta name="generator" content="https://cgit.ctu.cx/nimstagit">
+
+		<link href="/style.css" rel="stylesheet" />
+	</head>
+	<body>
+		<header>
+			<h1><a href="/">{{siteTitle}}</a>: {{repoName}}</h1>
+			<p>{{description}}</p>
+		</header>
+		<nav>
+            <a href="/{{repoUrl}}">summary</a>
+			<a href="/{{repoUrl}}/log" class="active">log</a>
+			<a href="/{{repoUrl}}/tree">tree</a>
+			<a href="/{{repoUrl}}/refs">refs</a>
+		</nav>
+		<main>
+		<div class="event-list">
+			<div class="event">
+				<a class="right" href="/{{repoUrl}}/log/{{id}}.html">{{when}} ago</a>
+				commit: {{id}}<br>
+				{{#parents}}
+				parent: <a href="/{{repoUrl}}/log/{{.}}.html">{{.}}</a><br>
+				{{/parents}}
+				author: {{authorName}}<br>
+				committer: {{committerName}}<br><br>
+				<pre>{{message}}</pre>
+			</div>			
+		</div>
+		</main>
+		<footer>Generated on {{generated}}</footer>
+	</body>
+</html>
+
diff --git a/src/assets/repoLog.html b/src/assets/repoLog.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>{{repoName}} - {{siteTitle}}</title>
+
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0">
+		<meta name="description" content="{{description}}">
+        <meta name="generator" content="https://cgit.ctu.cx/nimstagit">
+
+		<link href="/style.css" rel="stylesheet" />
+	</head>
+	<body>
+		<header>
+			<h1><a href="/">{{siteTitle}}</a>: {{repoName}}</h1>
+			<p>{{description}}</p>
+		</header>
+		<nav>
+            <a href="/{{repoUrl}}">summary</a>
+			<a href="/{{repoUrl}}/log" class="active">log</a>
+			<a href="/{{repoUrl}}/tree">tree</a>
+			<a href="/{{repoUrl}}/refs">refs</a>
+		</nav>
+		<main>
+			<div class="events">
+				{{#commits}}
+				<div class="event">
+					<a href="{{id}}.html">{{shortId}}</a> — {{authorName}}
+					<small class="right">{{when}} ago</small>
+					<pre>{{message}}</pre>
+				</div>
+				{{/commits}}
+			</div>
+			<!-- <table>
+                <thead>
+                    <tr>
+                        <td>id</td>
+                        <td>Commit message</td>
+                        <td>Author</td>
+                        <td>Date</td>
+                    </tr>
+                </thead>
+                {{#commits}}
+                <tr>
+                    <td><a href="{{id}}.html">{{shortId}}</a></td>
+                    <td style="text-overflow: ellipsis;"><a href="{{id}}.html">{{summary}}</a></td>
+                    <td>{{authorName}}</td>
+                    <td>{{when}} ago</td>
+                </tr>
+                {{/commits}}
+            </table> -->
+		</main>
+		<footer>Generated on {{generated}}</footer>
+	</body>
+</html>
+
diff --git a/src/assets/repoRefs.html b/src/assets/repoRefs.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+	<head>
+        <title>{{repoName}} - {{siteTitle}}</title>
+
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta name="description" content="{{description}}">
+        <meta name="generator" content="https://cgit.ctu.cx/nimstagit">
+
+        <link href="/style.css" rel="stylesheet" />
+	</head>
+	<body>
+		<header>
+			<h1><a href="/">{{siteTitle}}</a>: {{repoName}}</h1>
+			<p>{{description}}</p>
+		</header>
+		<nav>
+            <a href="/{{repoUrl}}">summary</a>
+			<a href="/{{repoUrl}}/log">log</a>
+			<a href="/{{repoUrl}}/tree">tree</a>
+			<a href="/{{repoUrl}}/refs" class="active">refs</a>
+		</nav>
+		<main>
+		</main>
+		<footer>Generated on {{generated}}</footer>
+	</body>
+</html>
+
diff --git a/src/assets/repoSummary.html b/src/assets/repoSummary.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>{{repoName}} - {{siteTitle}}</title>
+
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0">
+		<meta name="description" content="{{description}}">
+        <meta name="generator" content="https://cgit.ctu.cx/nimstagit">
+
+		<link href="/style.css" rel="stylesheet" />
+	</head>
+	<body>
+		<header>
+			<h1><a href="/">{{siteTitle}}</a>: {{repoName}}</h1>
+			<p>{{description}}</p>
+		</header>
+		<nav>
+            <a href="/{{repoUrl}}" class="active">summary</a>
+			<a href="/{{repoUrl}}/log">log</a>
+			<a href="/{{repoUrl}}/tree">tree</a>
+			<a href="/{{repoUrl}}/refs">refs</a>
+		</nav>
+		<main>
+			<div class="container">
+				<div class="row">
+					<div class="col-8">
+						<h3>last commits</h3>
+						<div class="events">
+							{{#lastCommits}}
+							<div class="event">
+								<a href="/{{repoUrl}}/log/{{id}}.html">{{shortId}}</a> — {{authorName}}
+								<small class="right">{{when}} ago</small>
+								<pre>{{summary}}</pre>
+							</div>
+							{{/lastCommits}}
+						</div>
+						<!-- <table>
+							<thead>
+								<tr>
+									<td>id</td>
+									<td>Commit message</td>
+									<td>author</td>
+									<td>date</td>
+								</tr>
+							</thead>
+							{{#lastCommits}}
+							<tr>
+								<td>{{shortId}}</td>
+								<td>{{summary}}</td>
+								<td>{{committerName}}</td>
+								<td>{{when}} ago</td>
+							</tr>
+							{{/lastCommits}}
+						</table> -->
+					</div>
+					<div class="col-4">
+						<h3>clone</h3>
+						<dl>
+							<dt>read-only</dt>
+							<dd><a href="https://git.ctu.cx/{{repoUrl}}">https://git.ctu.cx/{{repoName}}</a></dd>
+							<dt>read/write</dt>
+							<dd>git@wanderduene.ctu.cx:{{repoName}}</dd>
+						</dl>
+					</div>
+				</div>
+			</div>
+		<div class="responsive">
+		{{{readmeContent}}}
+		</div>
+		</main>
+		<footer><p>Generated on {{generated}}</p></footer>
+	</body>
+</html>
+
+   +
\ No newline at end of file
diff --git a/src/assets/repoTree.html b/src/assets/repoTree.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html>
+	<head>
+        <title>{{repoName}} - {{siteTitle}}</title>
+
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta name="description" content="{{description}}">
+        <meta name="generator" content="https://cgit.ctu.cx/nimstagit">
+
+        <link href="/style.css" rel="stylesheet" />
+	</head>
+	<body>
+		<header>
+			<h1><a href="/">{{siteTitle}}</a>: {{repoName}}</h1>
+			<p>{{description}}</p>
+		</header>
+		<nav>
+            <a href="/{{repoUrl}}">summary</a>
+			<a href="/{{repoUrl}}/log">log</a>
+			<a href="/{{repoUrl}}/tree" class="active">tree</a>
+            <a href="/{{repoUrl}}/refs">refs</a>
+		</nav>
+		<main>
+            {{#isSubdir}}
+			{{#path}}<a href="{{url}}">{{name}}</a> / {{/path}} {{dirName}}
+            {{/isSubdir}}
+            <table>
+                <thead>
+                    <tr>
+                        <td style="width: 10%">Mode</td>
+                        <td style="width: 80%">Name</td>
+                        <td style="width: 10%">Size</td>
+                    </tr>
+                </thead>
+                {{#isSubdir}}
+                <tr>
+                    <td></td>
+                    <td><a href="..">..</a></td>
+                    <td></td>
+                </tr>
+                {{/isSubdir}}
+                {{#entries}}
+                <tr>
+                    <td>{{mode}}</td>
+                    {{#isDir}}
+                    <td><a href="{{name}}/">{{name}}</a></td>
+                    {{/isDir}}
+                    {{^isDir}}
+                    <td><a href="{{name}}.html">{{name}}</a></td>
+                    {{/isDir}}
+                    <td>{{size}}</td>
+                </tr>
+                {{/entries}}
+            </table>
+		</main>
+		<footer>Generated on {{generated}}</footer>
+	</body>
+</html>
+
diff --git a/src/assets/style.css b/src/assets/style.css
@@ -0,0 +1,283 @@
+* {
+	font-family: monospace;
+	line-height: 1.25;
+}
+
+*, ::after, ::before {
+    box-sizing: border-box;
+}
+
+body {
+	margin: 0 auto;
+	max-width: 960px;
+}
+
+nav a {
+	color: #777;
+	text-decoration: none;
+}
+
+a {
+	color: black;
+	text-decoration: none;
+}
+
+a:hover {
+	text-decoration: underline;
+}
+
+header {
+	margin: 10px;
+}
+
+header h1 {
+	color: #000;
+	font-size: 2em;
+	font-weight: bold;
+	margin-bottom: 0px;
+}
+
+header p {
+	color: #777;
+	margin-top: 0px;
+}
+
+nav {
+	margin-top: 2em;
+	padding: 0px 1em;
+	vertical-align: bottom;
+}
+
+nav a {
+	padding: 2px 0.75em;
+	font-size: 110%;
+}
+
+nav a.active {
+	color: #000;
+	background-color: #ccc;
+}
+
+main {
+	padding: 2em;
+}
+
+pre {
+    background: #e9ecef;
+    padding: .25rem;
+    display: block;
+    font-size: 87.5%;
+    color: #212529;
+    margin: 0;
+    overflow: auto;
+}
+
+table {
+	width: 100%;
+}
+
+thead {
+	font-weight: bold;
+}
+
+footer {
+	margin-top: 0;
+	text-align: center;
+	font-size: 1em;
+	color: #ccc;
+}
+
+
+
+/***
+ *
+ *  events (e.g. commit)
+ *
+ ***/
+
+.event {
+	text-overflow: ellipsis;
+	overflow: hidden;
+	padding: .5rem;
+	margin: .5rem 0;
+	background: #f8f9fa;
+}
+
+.event :last-child {
+    margin-bottom: 0;
+}
+
+.event pre {
+	text-overflow: ellipsis;
+    overflow: none;
+    padding-left: 0;
+    padding-right: 0;
+    background: 0 0;
+	display: block;
+	font-size: 87.5%;
+	color: #212529;
+}
+
+
+
+/***
+ *
+ *  code viewer (blob page)
+ *
+ ***/
+
+.code {
+    display: grid;
+    grid-template-columns: auto auto auto 1fr;
+    grid-template-rows: auto;
+    background: #e9ecef;
+}
+
+.code .lines {
+    grid-column-start: 1;
+    grid-row-start: 1;
+    text-align: right;
+    padding-left: .5rem;
+    padding-right: .5rem;
+	padding-bottom: 1rem;
+    background: #eee;
+    border-right: 1px solid #444;
+    z-index: 0;
+}
+
+.code .lines a.selected::before, .code .lines a:target::before {
+    display: block;
+    content: "";
+    width: calc(960px - 4em - 5px);
+    z-index: -1;
+    position: absolute;
+    background: #b3d7ff;
+    margin-left: -6px;
+}
+
+.code .highlight {
+    grid-column-start: 2;
+    grid-row-start: 1;
+    padding-left: 1rem;
+    padding-bottom: 1rem;
+    background: 0 0;
+    z-index: 0;
+}
+
+
+
+/***
+ *
+ *  overview page
+ *
+ ***/
+
+td.sublevel-repo {
+	padding-left: 1.5em;
+}
+
+td.reposection {
+	color: #888;
+	font-style: italic;
+}
+
+
+
+/***
+ *
+ *  times
+ *
+ ***/
+
+span.age-mins {
+	font-weight: bold;
+	color: #080;
+}
+
+span.age-hours {
+	color: #080;
+}
+
+span.age-days {
+	color: #040;
+}
+
+span.age-weeks {
+	color: #444;
+}
+
+span.age-months {
+	color: #888;
+}
+
+span.age-years {
+	color: #bbb;
+}
+
+
+
+/***
+ *
+ *  grid-system
+ *
+ ***/
+
+.container {
+	width: 100%;
+	margin-left: -2%;
+	margin-right: -2%;
+}
+
+.row {
+	position: relative;
+	width: 100%;
+}
+
+.row [class^="col"] {
+	float: left;
+	margin: 0 2%;
+	min-height: 0.125rem;
+}
+
+.row::after {
+	content: "";
+	display: table;
+	clear: both;
+}
+
+
+.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12 {
+	width: 96%;
+}
+
+@media only screen and (min-width: 45em) {  /* 720px */
+	.col-1  { width: 4.33%; }
+	.col-2  { width: 12.66%; }
+	.col-3  { width: 21%; }
+	.col-4  { width: 29.33%; }
+	.col-5  { width: 37.66%; }
+	.col-6  { width: 46%; }
+	.col-7  { width: 54.33%; }
+	.col-8  { width: 62.66%; }
+	.col-9  { width: 71%; }
+	.col-10 { width: 79.33%; }
+	.col-11 { width: 87.66%; }
+	.col-12 { width: 96%; }
+}
+
+
+
+/***
+ *
+ *  helper
+ *
+ ***/
+
+.responsive {
+	width: 100%;
+	overflow: auto;
+}
+
+.right {
+	float: right;
+}
diff --git a/src/index.nim b/src/index.nim
@@ -0,0 +1,67 @@
+import os, posix, times, json, tables, algorithm, uri
+import nimgit
+
+import types, utils
+
+
+type
+    Repository = object
+        repoName:     string
+        repoUrl:      string
+        description:  string
+        lastActivity: string
+        objId:        string
+
+    Category = object
+        name:  string
+        repos: seq[Repository]
+
+proc reposOverview* (config: Config): seq[Category] =
+    template getRepoMetadata = 
+        try:
+            let
+              repoName      = nameFromPath(repoPath)
+              repository    = openGitRepository(repoPath)
+              config        = repository.config
+              objId         = repository.lookupObjectIdByName("HEAD")
+              commit        = repository.lookupCommit(objId)
+              category      = config.get("gitweb.category")
+
+            if not categoriesTable.hasKey(category):
+                categoriesTable[category] = newSeq[Repository]()
+
+            categoriesTable[category].add(Repository(
+                    repoName:     repoName,
+                    repoUrl:      encodeUrl(repoName),
+                    description:  config.get("gitweb.description"),
+                    lastActivity: relativeTimeFromNow(commit.time.time),
+                    objId:        $objId
+                ))
+
+            free(commit)
+            free(objId)
+            free(config)
+            free(repository)
+
+        except:
+            echo "This seems to not be a git-repo: " & repoPath
+            echo "Error:\n" & getCurrentExceptionMsg()
+
+    var categoriesTable = initTable[string, seq[Repository]]()
+
+    if config.projectsList.len != 0:
+        for element in config.projectsList:
+            let repoPath = joinPath(config.scanPath, element)
+            getRepoMetadata()
+
+    for name, repos in categoriesTable:
+        var repos = repos
+
+        repos.sort(proc (x, y: Repository): int = cmp(x.repoName, y.repoName))
+        
+        result.add(Category(
+                name: name,
+                repos: repos
+            ))
+
+    result.sort(proc (x, y: Category): int = cmp(x.name, y.name))
diff --git a/src/nimstagit.nim b/src/nimstagit.nim
@@ -0,0 +1,58 @@
+import os, times, json
+import nimgit, moustachu
+
+import types, utils, assets
+
+import index, repoGenerator
+
+
+var config {.threadvar.}: Config
+
+proc main =
+    if paramCount() == 0:
+        echo "No options given!"
+        quit(QuitFailure)
+
+    if paramCount() == 1:
+        echo "No config given!"
+        quit(QuitFailure)
+
+    discard git_libgit2_init()
+
+    config = readConfig(paramStr(2))
+
+    if not dirExists(config.outputDirectory):
+        echo "Output-directory does not exist!"
+        quit(QuitFailure)
+
+    case paramStr(1):
+        of "index":
+            let templateContext = %* {
+                    "siteTitle":   config.title,
+                    "description": config.description,
+                    "generated":   $now(),
+                    "categories":  reposOverview(config)
+                }
+
+            echo "Generate page: index.html"
+            writeFile(joinPath(config.outputDirectory, "index.html"), render(templateOverview, templateContext))
+            echo "Create asset: style.css"
+            writeFile(joinPath(config.outputDirectory, "style.css"), assetStyleCss)
+
+        of "repo":
+            if paramCount() > 2:
+                if not config.projectsList.contains(paramStr(3)):
+                    echo "The repo '" & paramStr(3) & "' is not in the projectsList/scanDir!"
+                    quit(QuitFailure)
+
+                repoGenerator(config, paramStr(3))
+
+            else:
+                for repo in config.projectsList:
+                    repoGenerator(config, repo)
+
+        else:
+            echo "Unknown option: " & paramStr(1)
+            echo "Valid options are: index,"
+
+main()+
\ No newline at end of file
diff --git a/src/repoGenerator.nim b/src/repoGenerator.nim
@@ -0,0 +1,352 @@
+import os, json, times, uri, strutils, sequtils
+import moustachu, nimgit, markdown
+
+import types, utils, assets
+
+type
+    CommitSummary = object
+        id:                string
+        shortId:           string
+        `when`:            string
+        committerIsAuthor: bool
+        authorName:        string
+        authorMail:        string
+        committerName:     string
+        committerMail:     string
+        summary:           string
+        body:              string
+        message:           string
+
+    RepoLog = object
+        description:       string
+        category:          string
+        commits:           seq[CommitSummary]
+
+    RepoTreeEntry = object
+        isDir:             bool
+        name:              string
+        mode:              string
+        size:              string
+
+    RepoTree = object
+        description:       string
+        category:          string
+        entries:           seq[RepoTreeEntry]
+
+    PathObj = object
+        name:              string
+        url:               string
+
+
+
+proc createCommitSummary (commit: GitCommit): CommitSummary =
+    let author    = commit.author
+    let committer = commit.committer
+
+    result.id                = $commit.id
+    result.shortId           = commit.shortId
+    result.when              = relativeTimeFromNow(commit.time.time)
+    result.committerIsAuthor = (author.email == committer.email)
+    result.authorName        = author.name
+    result.authorMail        = author.email
+    result.committerName     = committer.name
+    result.committerMail     = committer.email
+    result.summary           = commit.summary
+    result.body              = commit.body
+    result.message           = commit.message
+
+
+proc generateCommitPage (path: string, templateContext: JsonNode, commit: GitCommit) =
+#    if fileExists(joinPath(joinPath(path, $commit.id & ".html"))): return
+
+    let
+      id        = $commit.id
+      author    = commit.author
+      committer = commit.committer
+
+    var parents: seq[string]
+
+    if commit.parentCount != 0:
+        for id in commit.parentIds:
+            parents.add($id)
+
+
+    writeFile(
+        joinPath(joinPath(path, id & ".html")),
+        render(
+            templateRepoCommit,
+            mergeJson(templateContext, %* {
+                "id":                id,
+                "shortId":           commit.shortId,
+                "when":              relativeTimeFromNow(commit.time.time),
+                "message":           commit.message,
+                "committerIsAuthor": (committer.email == author.email),
+                "committerName":     committer.name,
+                "committerMail":     committer.email,
+                "authorName":        author.name,
+                "authorMail":        author.email,
+                "parents":           parents
+            })
+        )
+    )
+
+
+proc generateRepoBlobPage (path: string, pathSeq: seq[PathObj], templateContext: JsonNode, entry: GitTreeEntry, blob: GitBlob) =
+    let content = blob.content
+
+    var blobData = %*{
+            "path":     pathSeq,
+            "id":       $entry.id,
+            "filename": entry.name,
+            "filesize": formatSize(blob.size),
+            "isBinary": blob.isBinary
+        }
+
+    if not blob.isBinary:
+        blobData["content"] = %content
+        blobData["lines"]   = %toSeq(1..content.countLines)
+
+    writeFile(joinPath(path, entry.name), content)
+
+    writeFile(
+        joinPath(joinPath(path, entry.name & ".html")),
+        render(
+            templateRepoBlob,
+            mergeJson(templateContext, blobData)
+        )
+    )
+
+proc generateRepoTreePage (isSubdir: bool, path: string, pathSeq: seq[PathObj], dirName: string, templateContext: JsonNode, tree: GitTree) =
+    discard existsOrCreateDir(joinPath(path))
+
+    var
+      entries     : seq[RepoTreeEntry]
+      tempPathSeq = pathSeq
+
+    if isSubdir:
+        tempPathSeq.add(PathObj(
+                name: dirName,
+                url: pathSeq[pathSeq.len-1].url & "/" & encodeUrl(dirName)
+            ))
+
+    for entry in tree:
+        var size: int
+
+        if entry.type == goBlob:
+            let blob = tree.owner.lookupBlob(entry.id)
+
+            tempPathSeq.add(PathObj(
+                    name: dirName,
+                    url: tempPathSeq[tempPathSeq.len-1].url & "/" & encodeUrl(dirName)
+                ))
+
+            tempPathSeq.delete(tempPathSeq.len-1)
+
+            generateRepoBlobPage(path, tempPathSeq, templateContext, entry, blob)
+
+            size = blob.size
+            
+            free(blob)
+
+        entries.add(RepoTreeEntry(
+                isDir:    (entry.type != goBlob),
+                name:     entry.name,
+                mode:     entry.modeStr,
+                size:     formatSize(size)
+            ))
+
+        if entry.type == goTree:
+            let subtree = tree.owner.lookupTree(entry.id)
+
+            generateRepoTreePage(true, joinPath(path, entry.name), tempPathSeq, entry.name, templateContext, subtree)
+    
+            free(subtree)
+
+        free(entry)
+
+    writeFile(
+        joinPath(path, "index.html"),
+        render(
+            templateRepoTree,
+            mergeJson(templateContext, %*{
+                    "isSubdir": isSubdir,
+                    "path":     pathSeq,
+                    "dirName":  dirName,
+                    "entries":  entries
+                }
+            )
+        )
+    )
+
+
+proc repoGenerator* (config: Config, name: string) =
+    var
+      repository: GitRepository
+      repoConfig: GitConfig
+
+    try:
+        repository        = openGitRepository(joinPath(config.scanPath, name))
+        repoConfig        = repository.config
+
+        let
+          repoName        = nameFromPath(name)
+          description     = repoConfig.get("gitweb.description")
+          category        = repoConfig.get("gitweb.category")
+          templateContext = %* {
+                    "siteTitle":     config.title,
+                    "repoName":      repoName,
+                    "repoUrl":       encodeUrl(repoName),
+                    "generated":     $now(),
+                    "description":   description,
+                    "category":      category,
+                }
+
+
+        echo "Generate pages for repo: " & repoName
+        discard existsOrCreateDir(joinPath(config.outputDirectory, repoName))
+        discard existsOrCreateDir(joinPath(config.outputDirectory, repoName, "log"))
+        discard existsOrCreateDir(joinPath(config.outputDirectory, repoName, "tree"))
+        discard existsOrCreateDir(joinPath(config.outputDirectory, repoName, "refs"))
+
+
+        #
+        # Summary page
+        #
+
+        let
+          headObjId    = repository.lookupObjectIdByName("HEAD")
+          headCommit   = repository.lookupCommit(headObjId)
+          tree         = headCommit.tree
+          parentCount  = headCommit.parentCount
+
+        var lastCommits:   seq[CommitSummary]
+        var readmeContent: string
+
+        lastCommits.add(createCommitSummary(headCommit))
+
+        if parentCount != 0:
+            let parentCommit = repository.lookupCommit(headCommit.parentIds[0])
+            lastCommits.add(createCommitSummary(parentCommit))
+            free(parentCommit)
+
+        for readmeFile in config.readmeFiles:
+            if tree.entries.contains(readmeFile):
+                let 
+                  entry = tree.entry(readmeFile)
+                  blob  = repository.lookupBlob(entry.id)
+
+                if not config.renderMarkdown:
+                    readmeContent = blob.content
+                else:
+                    readmeContent = markdown(blob.content, config=initGfmConfig())
+
+                free(blob)
+                free(entry)
+                break
+
+
+        echo "Generate repo-summary page"
+        writeFile(
+            joinPath(config.outputDirectory, repoName, "index.html"), 
+            render(
+                templateRepoSummary,
+                mergeJson(
+                    templateContext,
+                    %* {
+                        "lastCommits":   lastCommits,
+                        "readmeContent": readmeContent
+                    }
+                )
+            )
+        )
+
+        #
+        # END Summary page
+        #
+
+        #
+        # Tree pages
+        #
+
+        var pathSeq: seq[PathObj]
+
+        pathSeq.add(PathObj(
+                name: "root",
+                url: "/" & encodeUrl(repoName) & "/tree"
+            ))
+
+        generateRepoTreePage(false, joinPath(config.outputDirectory, repoName, "tree"), pathSeq, "", templateContext, tree)
+
+        #
+        # END Tree pages
+        #
+
+
+        #
+        # Log page and Commit pages
+        #
+
+        let gitRevisionWalker = repository.walk()
+        var commits: seq[CommitSummary]
+
+        gitRevisionWalker.sort(GIT_SORT_TOPOLOGICAL)
+        gitRevisionWalker.pushHead()
+
+        for objectId in gitRevisionWalker:
+            let commit = repository.lookupCommit(objectId)
+
+            generateCommitPage(joinPath(config.outputDirectory, repoName, "log"), templateContext, commit)
+            commits.add(createCommitSummary(commit))
+
+            free(commit)
+
+        echo "Generate repo-commit and log page(s)"
+        writeFile(
+            joinPath(config.outputDirectory, repoName, "log/index.html"), 
+            render(
+                templateRepoLog,
+                mergeJson(
+                    templateContext,
+                    %* {
+                        "commits": commits
+                    }
+                )
+            )
+        )
+
+        #
+        # END Log page and Commit pages
+        #
+
+
+        #
+        # Log page and Commit pages
+        #
+
+        echo "Generate repo-refs page!"
+        writeFile(
+            joinPath(config.outputDirectory, repoName, "refs/index.html"), 
+            render(
+                templateRepoRefs,
+                mergeJson(
+                    templateContext,
+                    %* {
+                    }
+                )
+            )
+        )
+
+        #
+        # END Log page and Commit pages
+        #
+
+        free(tree)
+        free(headCommit)
+        free(headObjId)
+        free(repoConfig)
+        free(repository)
+
+    except:
+        free(repoConfig)
+        free(repository)
+        echo "Error:\n", getCurrentExceptionMsg()
diff --git a/src/types.nim b/src/types.nim
@@ -0,0 +1,9 @@
+type
+    Config* = object
+        outputDirectory*: string
+        title*:           string
+        description*:     string
+        scanPath*:        string
+        projectsList*:    seq[string]
+        readmeFiles*:     seq[string]
+        renderMarkdown*:  bool+
\ No newline at end of file
diff --git a/src/utils.nim b/src/utils.nim
@@ -0,0 +1,79 @@
+import os, times, parsecfg, strutils, json
+
+import types
+
+proc readConfig* (path: string): types.Config =
+  if not fileExists(path):
+    echo "Error: config-file does not exist!"
+    quit(QuitFailure)
+
+  let configFile = loadConfig(path)
+
+  let
+    outputDirectory = configFile.getSectionValue("", "outputDirectory", "")
+    title           = configFile.getSectionValue("", "title",           "nimstagit")
+    description     = configFile.getSectionValue("", "description",     "")
+    scanPath        = configFile.getSectionValue("", "scanPath",        "")
+    projectsFile    = configFile.getSectionValue("", "projectsFile",    "")
+    readmeFiles     = configFile.getSectionValue("", "readmeFiles",     "README, readme, README.md, readme.md")
+    renderMarkdown  = configFile.getSectionValue("", "renderMarkdown",  "true").parseBool
+
+  var projectsList: seq[string]
+
+  if scanPath == "":
+    echo "Error: config-value 'outputDirectory' has to be set!"
+    quit(QuitFailure)
+
+  if scanPath == "":
+    echo "Error: config-value 'scanPath' has to be set!"
+    quit(QuitFailure)
+
+  if projectsFile != "":
+    if not fileExists(projectsFile):
+      echo "Error: projectsFile does not exist!"
+      quit(QuitFailure)
+
+    for element in readFile(projectsFile).splitLines:
+      if element != "": projectsList.add(element)
+
+  else:
+    for kind, element in walkDir(scanPath):
+      if kind != pcDir: continue
+      projectsList.add(splitPath(element).tail)
+
+  result = types.Config(
+        outputDirectory:   outputDirectory,
+        title:             title,
+        description:       description,
+        scanPath:          scanPath,
+        projectsList:      projectsList,
+        readmeFiles:       readmeFiles.split(", "),
+        renderMarkdown:    renderMarkdown
+      )
+
+proc mergeJson* (a: JsonNode, b: JsonNode): JsonNode =
+  result = a
+  for key, val in b:
+      result[key] = val 
+
+proc nameFromPath* (path: string): string = result = splitPath(path).tail; removeSuffix(result, ".git")
+
+proc relativeTimeFromNow* (time: Time): string =
+  let
+    timeNow  = now().toTime
+    timeDiff = timeNow - time
+    weeks    = inWeeks(timeDiff)
+    days     = inDays(timeDiff)
+    hours    = inHours(timeDiff)
+    minutes  = inMinutes(timeDiff)
+
+  if weeks > 0:
+      result = $weeks & " week(s)"
+  elif days > 0:
+      result = $days & " day(s)"
+  elif hours > 0:
+      result = $hours & " hour(s)"
+  elif minutes > 2:
+      result = $minutes & " Minutes"
+  else:
+      result = "now"