views.py
1 |
|
2 |
Copyright © 2016 Maarten "Vngngdn" Vangeneugden |
3 |
|
4 |
This program is free software: you can redistribute it and/or modify |
5 |
it under the terms of the GNU Affero General Public License as |
6 |
published by the Free Software Foundation, either version 3 of the |
7 |
License, or (at your option) any later version. |
8 |
|
9 |
This program is distributed in the hope that it will be useful, |
10 |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 |
GNU Affero General Public License for more details. |
13 |
|
14 |
You should have received a copy of the GNU Affero General Public License |
15 |
along with this program. If not, see https://www.gnu.org/licenses/agpl.html. |
16 |
""" |
17 |
|
18 |
from django.shortcuts import get_object_or_404, render # This allows to render the template with the view here. It's pretty cool and important. |
19 |
from django.http import HttpResponseRedirect, HttpResponse |
20 |
from django.urls import reverse |
21 |
from .models import * |
22 |
from .SvgBuilder import * |
23 |
|
24 |
from .GitActions import RepoInfo, FileInfo, CommitInfo |
25 |
from django.utils.translation import gettext as _ |
26 |
|
27 |
from git import Repo # GitPython functionality. |
28 |
import git |
29 |
import pydriller |
30 |
|
31 |
import datetime |
32 |
|
33 |
from .syntax import * |
34 |
|
35 |
|
36 |
# First, I list some standard variables that are common for most of the sites of this app. |
37 |
|
38 |
def footer_description(): |
39 |
return _("Gitar is a simple web app that allows its users to easily share Git repos in combination with the Django framework.") |
40 |
|
41 |
def footer_links(): |
42 |
footer_links = [ |
43 |
[_('Home page'), reverse('about-index')], |
44 |
#[_('Source code'), reverse('gitar-repository', kwargs={'repository_name':'gitar'})], |
45 |
] |
46 |
return footer_links |
47 |
|
48 |
def standard_context(): |
49 |
context = { |
50 |
'navbar_title': "Gitar", |
51 |
'navbar_backArrow': False, |
52 |
'footer_title': "Gitar", |
53 |
'footer_description': footer_description(), |
54 |
'footer_links': footer_links(), |
55 |
'stylesheet_name': "gitar", |
56 |
} |
57 |
return context |
58 |
|
59 |
def download_tar(request): |
60 |
pass |
61 |
def download_git(request): |
62 |
pass |
63 |
def commit(request, repository_name, commit_hash): |
64 |
template = "gitar/commit.djhtml" |
65 |
|
66 |
repository_model = RepoInfo.get_repository_model(repository_name) |
67 |
commit_repo = pydriller.Repository(repository_model.directory_path, single=commit_hash) |
68 |
context = standard_context() |
69 |
#file_html_code = dict() |
70 |
context["modified_files"] = list() |
71 |
# NOTE: Although this looks like a for loop, it's not: It's a generator that |
72 |
# will only produce a single commit. But since it's a generator, I can't |
73 |
# extract the first element with indexing. So this will have to do. |
74 |
for c in commit_repo.traverse_commits(): |
75 |
context["commit"] = c |
76 |
# Splitting up the message file correctly |
77 |
# case: Only summary line, no \n: |
78 |
if '\n' not in c.msg: |
79 |
context["first_line"] = c.msg |
80 |
# case: Summary line split by two \n: |
81 |
elif '\n' not in c.msg.split('\n\n', maxsplit=1)[0]: |
82 |
parts = c.msg.split('\n\n', maxsplit=1) |
83 |
context["first_line"] = parts[0] |
84 |
context["other_lines"] = parts[1] |
85 |
# case: Badly formatted message |
86 |
else: |
87 |
parts = c.msg.split('\n', maxsplit=1) |
88 |
context["first_line"] = parts[0] |
89 |
context["other_lines"] = parts[1] |
90 |
# Final formatting: |
91 |
if "other_lines" in context: |
92 |
context["other_lines"] = context["other_lines"].replace('\n\n','<br>').replace('\n', ' ') |
93 |
# Processing the modified files |
94 |
for modified_file in c.modified_files: |
95 |
#print("FILE") |
96 |
processed = CommitInfo.prepare_and_process(modified_file) |
97 |
#print(processed) |
98 |
context["modified_files"].append(processed) |
99 |
#html_code_before = None |
100 |
#html_code_after = None |
101 |
#if modified_file.source_code_before is not None: |
102 |
#html_code_before = code_to_HTML( |
103 |
#modified_file.source_code_before, |
104 |
#modified_file.filename) |
105 |
#if modified_file.source_code is not None: |
106 |
#html_code_after = code_to_HTML( |
107 |
#modified_file.source_code, |
108 |
#modified_file.filename) |
109 |
# XXX: This function WILL OVERWRITE the presented code of a file if |
110 |
# this particular commit includes multiple files with the same name, |
111 |
# but a different path. I'll update this in the future... |
112 |
#file_html_code[modified_file.filename] = (html_code_before, html_code) |
113 |
#context["file_html_code"] = file_html_code |
114 |
|
115 |
#context["subdirectories"] = subdirectories |
116 |
#context["commits"] = commits |
117 |
#context["branch"] = branch |
118 |
context["repository_name"] = repository_name |
119 |
# Adding the html code for the files |
120 |
#context["file_html_code"] |
121 |
#context["repository_description"] = repository.description |
122 |
#html_code = code_to_HTML(raw_file_data, file.name) |
123 |
|
124 |
return render(request, template, context) |
125 |
|
126 |
def file_commit(request): |
127 |
pass |
128 |
|
129 |
# From here, the actual views start. |
130 |
def index(request): |
131 |
""" The start page of Gitar. |
132 |
|
133 |
The goal of this view, is to collect all the available repositories, |
134 |
including some additional information, such as programming language, |
135 |
license, description, ... in order to give a fast overview of the most |
136 |
prominent information. |
137 |
""" |
138 |
|
139 |
# Collecting the available repositories: |
140 |
# Template: |
141 |
template = "gitar/index.djhtml" |
142 |
# Requesting the repositories: |
143 |
modelRepos = Repository.objects.all() |
144 |
# From here, we start collecting info about all the repositories: |
145 |
class BlankRepository: pass # Blank object in which all data will be collected. |
146 |
repositories = [] |
147 |
for modelRepo in modelRepos: |
148 |
repository = BlankRepository() |
149 |
# TODO: Find a way to add all of modelRepo's fields without having to |
150 |
# hardcode them. This is prone to errors and is redundant. |
151 |
repository.name = str(modelRepo) |
152 |
repository.programmingLanguage = modelRepo.programmingLanguage |
153 |
repository.license = modelRepo.license |
154 |
repository.description = RepoInfo.get_description(modelRepo) |
155 |
|
156 |
#gitRepo = Repo.init(modelRepo.directory(), bare=True) # Connects to the Git Repo. |
157 |
# See tests.py, which assures all repositories exist. Tests are handy. |
158 |
#repository.description = gitRepo.description |
159 |
# This is mostly personal taste, but I like to show the amount of files. |
160 |
#repoTree = gitRepo.heads.master.commit.tree |
161 |
#repository.fileCount = len(repoTree.blobs) # blobs are files. |
162 |
repositories.append(repository) |
163 |
# After that, I extend the standard context with the repositories: |
164 |
context = standard_context() |
165 |
context['repositories'] = repositories |
166 |
# And finally, sending everything back. |
167 |
return render(request, template, context) |
168 |
|
169 |
def repositories(request, repository_name, branch="master"): |
170 |
# A repo's root is a directory by default, so this will automatically return |
171 |
# a directory view. But still, this is a bit nicer. |
172 |
return path_explorer(request, repository_name, branch, "") |
173 |
|
174 |
def path_explorer(request, repository_name, branch, path=""): |
175 |
""" Checks whether the given path is a file or a directory, and calls the |
176 |
appropriate view function accordingly. |
177 |
""" |
178 |
repository = RepoInfo.get_repository_object(repository_name) |
179 |
# From the GitPython documentation: |
180 |
# You can obtain the tree object of a repository, which is the directory of |
181 |
# that repo. This tree can be accessed as if it were a native Python list, |
182 |
# where the elements are the subdirectories and files. So, the idea to |
183 |
# determine whether a file, or a directory was requested, is simple: |
184 |
# 1. Split the path with "/" as seperator. |
185 |
# 2. Replace the current tree variable with the one retrieved from the |
186 |
# subtree element |
187 |
# 3. Repeat 2. until all parts of the given path are exhausted. |
188 |
# If we now still have a tree, we're looking at a directory, so display the |
189 |
# files (and subdirectories) of this directory. |
190 |
# Else, if we hit a blob, display the file contents. |
191 |
path_parts = path.split(sep="/") |
192 |
# FIXME: This is a bug at the URL regex part that I haven't been able to fix |
193 |
# yet. This serves as a temporary fix: |
194 |
# If the last part of the path is an empty string (which happens when the |
195 |
# last symbol was a '/'), remove that part from the list. |
196 |
# Of course, this is bad monkeypatching, but I suck at regex, so as long as |
197 |
# I don't find the solution, this'll have to do. |
198 |
|
199 |
|
200 |
#print(path_parts) |
201 |
|
202 |
if path_parts[len(path_parts)-1] == "": |
203 |
path_parts.pop() |
204 |
|
205 |
if len(path_parts) == 0: |
206 |
directory = repository.heads[branch].commit.tree |
207 |
return directory_view(request, repository_name, branch, path, directory) |
208 |
|
209 |
assert len(path_parts) != 0 |
210 |
|
211 |
# FIXME: If the user gives a "<something>/../<somethingElse>", that should |
212 |
# become "<something>". Obviously, although I think that's done by default |
213 |
# already. |
214 |
directory = repository.heads[branch].commit.tree |
215 |
for i in range(len(path_parts)): |
216 |
subdirectories = directory.trees |
217 |
#if len(subdirectories) == 0: |
218 |
# This can't happen, as this would imply there is a directory inside |
219 |
# a file. |
220 |
# assert False |
221 |
#else: |
222 |
for subdirectory in subdirectories: |
223 |
if subdirectory.name == path_parts[i]: |
224 |
directory = subdirectory |
225 |
#break # Useless optimization |
226 |
# When there are no more directories to traverse, check if the last part of |
227 |
# the path is either a file, or a directory: |
228 |
blobs = directory.blobs |
229 |
#print(path_parts) |
230 |
last_part = path_parts[len(path_parts)-1] |
231 |
for blob in directory.blobs: |
232 |
#print(blob.name) |
233 |
if blob.name == last_part: |
234 |
file_blob = blob |
235 |
#print("Returning file view") |
236 |
return file_view(request, repository_name, branch, path, file_blob) |
237 |
else: |
238 |
pass |
239 |
#print("blob name: " + blob.name) |
240 |
#print("last part: " + last_part) |
241 |
return directory_view(request, repository_name, branch, path, directory) |
242 |
|
243 |
def directory_view(request, repository_name, branch, path, directory): |
244 |
""" Collects the given directories's files and subdirectories, and renders a |
245 |
template to display this data. |
246 |
""" |
247 |
|
248 |
# Collecting files in this directory |
249 |
repository = RepoInfo.get_repository_object(repository_name) |
250 |
files = [] |
251 |
for file in directory.blobs: |
252 |
latest_commit_object = FileInfo.last_commit(repository_name, branch, file.path) |
253 |
older_than_one_month = (datetime.datetime.now(datetime.timezone.utc) - latest_commit_object.committer_date).days > 30 |
254 |
#print(latest_commit_object) |
255 |
files.append({ |
256 |
"name":file.name, |
257 |
"path":file.path, |
258 |
#"commit":"",#FileInfo.last_commit(repository, file).hexsha[:20], |
259 |
"commit": latest_commit_object, |
260 |
"older_than_one_month": older_than_one_month, |
261 |
}) |
262 |
#print(FileInfo.last_commit(repository_name, branch, file)) |
263 |
# Collecting commits for this branch |
264 |
commits = [] |
265 |
for commit in repository.iter_commits(branch): |
266 |
commits.append({ |
267 |
"hash":commit.hexsha[:20], |
268 |
"author":commit.author, |
269 |
"description":commit.summary, |
270 |
"date": datetime.datetime.fromtimestamp(commit.committed_date), |
271 |
}) |
272 |
# Collecting subdirectories |
273 |
subdirectories = [] |
274 |
for subdirectory in directory.trees: |
275 |
subdirectories.append({ |
276 |
"path":subdirectory.path, |
277 |
"name":subdirectory.name, |
278 |
}) |
279 |
# Collecting rendering information: |
280 |
template = "gitar/directory.djhtml" |
281 |
context = standard_context() |
282 |
context["files"] = files |
283 |
context["subdirectories"] = subdirectories |
284 |
context["commits"] = commits |
285 |
context["branch"] = branch |
286 |
context["repository_name"] = repository_name |
287 |
context["repository_description"] = repository.description |
288 |
# Collection repo information |
289 |
for repo in Repository.objects.all(): |
290 |
if str(repo) == repository_name: |
291 |
context["repository_language"] = repo.programmingLanguage |
292 |
context["repository_license"] = repo.license |
293 |
break |
294 |
branches = [] |
295 |
for bbranch in repository.heads: |
296 |
branches.append(bbranch.name) |
297 |
context["branches"] = branches |
298 |
|
299 |
|
300 |
# DEBUG |
301 |
shares = RepoInfo.get_directory_source_share(directory) |
302 |
context["shares_svg"] = language_share_svg(shares) |
303 |
tag_colours = dict() |
304 |
|
305 |
unknown_percentage = 1.0 |
306 |
for language in shares.keys(): |
307 |
unknown_percentage -= shares[language] |
308 |
shares[language] = (shares[language], language_css_gradient(language)) |
309 |
context["file_shares"] = sorted(shares.items(), key=lambda x: x[1])[::-1] |
310 |
context["unknown_percentage"] = max(0.01, unknown_percentage) if unknown_percentage != 0.0 else 0 |
311 |
#context["tag_colours"] = tag_colours |
312 |
#print(tag_colours) |
313 |
# END DEBUG |
314 |
return render(request, template, context) |
315 |
|
316 |
|
317 |
def file_view(request, repository_name, branch, path, file): |
318 |
""" Collects the file contents of the given file path, and returns it to the |
319 |
template, with the file contents already formatted in HTML using Pygments. |
320 |
""" |
321 |
|
322 |
# Turning the file's contents in HTML ready output: |
323 |
raw_file_data = file.data_stream.read() |
324 |
html_code = code_to_HTML(raw_file_data, file.name) |
325 |
# Collecting rendering information: |
326 |
template = "gitar/file.djhtml" |
327 |
context = standard_context() |
328 |
context["content"] = html_code |
329 |
context["file_name"] = file.name |
330 |
context["repository_name"] = repository_name |
331 |
return render(request, template, context) |
332 |
|
333 |