gitar

views.py

1
""" views.py - Describes the different views that can be presented to the user.
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