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