gitar

Major additions to repo displaying

The Directory template has been given a visual overhaul, and now fully supports different screen sizes. Furthermore, the amount of data displayed has been increased, and the handling of links in the templates and in the URLConf has been improved.
In the view module, the functions have been adapted so they can provide all the necessary information to the templates.
Next improvements will consist of bugfixes and handling the displaying of files.

Author
Maarten 'Vngngdn' Vangeneugden
Date
Oct. 19, 2017, 2:39 a.m.
Hash
0b0c8ac62f4d6f1e41704697f2dbf5580f6e8703
Parent
ac8c428e67d325522d7d81135b28527241280754
Modified files
GitActions/RepoInfo.py
templates/gitar/directory.html
templates/gitar/file.html
urls.py
views.py

GitActions/RepoInfo.py

20 additions and 0 deletions.

View changes Hide changes
1
1
    Copyright © 2016 Maarten "Vngngdn" Vangeneugden
2
2
3
3
    This program is free software: you can redistribute it and/or modify
4
4
    it under the terms of the GNU Affero General Public License as
5
5
    published by the Free Software Foundation, either version 3 of the
6
6
    License, or (at your option) any later version.
7
7
8
8
    This program is distributed in the hope that it will be useful,
9
9
    but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
    GNU Affero General Public License for more details.
12
12
13
13
    You should have received a copy of the GNU Affero General Public License
14
14
    along with this program. If not, see https://www.gnu.org/licenses/agpl.html.
15
15
"""
16
16
17
17
from ..models import Repository
18
18
import git
19
19
20
20
def get_description(repository):
21
21
    """ Returns the Git repo description of the given repository.
22
22
    """
23
23
    if isinstance(repository, Repository):
24
24
        repository = git.Repo(repository.directory_path)
25
25
26
26
    return repository.description
27
27
28
28
def log_message(repository, commit, simple=True):
29
29
    """ Returns the log message that was attached to the given commit.
30
30
31
31
    Keyword arguments:
32
32
    repository -- the repository in which to search
33
33
    commit     -- the specific commit hash
34
34
    simple     -- whether to return a oneliner, or the entire message (default True)
35
35
    """
36
36
    pass
37
37
38
38
39
39
def get_repository_object(repository_name):
40
40
    """ Checks the database for a repository with the same name, and returns a
41
41
    GitPython Repo object.
42
42
43
43
    Given the name of the repository, this function will search the database for
44
44
    a repository whoms name corresponds with the given name. When it found one, 
45
45
46
46
    Keyword arguments:
47
47
    repository_name -- The name of the repository
48
48
    """
49
49
    # Next line raises a Repository.DoesNotExist exception if not found, so it's
50
50
    # not necessary to check whether it was found or not.
51
51
    repository = Repository.objects.get(directory_path__endswith=repository_name+".git")
52
52
53
53
    return git.Repo(repository.directory_path)
54
54
55
55
def get_repository_model_object(repository_name):
56
56
    """ Functions identical to the get_repository_object, except that this
57
57
    function returns the Django model representation.
58
58
59
59
    Keyword arguments:
60
60
    repository_name -- The name of the repository
61
61
    """
62
62
    return Repository.objects.get(directory_path__endswith=repository_name+".git")
63
63
64
64
def read_file(file_blob):
65
65
    """ Reads the contents of the given file, and returns it in a list of
66
66
    strings.
67
67
68
68
    Reading the contents of a file using GitPython is a bit cumbersome. This
69
69
    function takes care of the hassle, and returns a list of unicode strings,
70
70
    allowing easy operations on the file's contents.
71
71
    """
72
72
73
73
    file_data_stream = file_blob.data_stream
74
74
    file_content = file_data_stream.read().decode("utf-8")
75
75
    file_formatted_content = []
76
76
    line = ""
77
77
    for character in file_content:
78
78
        if character != "\n":
79
79
            line = line + character
80
80
        else:
81
81
            file_formatted_content.append(line)
82
82
            line = ""
83
83
    return file_formatted_content
84
84
+
85
def get_branches(repository):
+
86
    """ Returns all branch objects of the repository.
+
87
    """
+
88
    return repository.heads
+
89
+
90
def get_commits_of_all_branches(repository):
+
91
    """ Returns a dict with the keys being the branch objects, and the values
+
92
    being their commits.
+
93
    """
+
94
    heads = repository.heads
+
95
    branches = dict()
+
96
    for head in heads:
+
97
        branches[head] = get_commits(repository, head.name)
+
98
    return branches
+
99
def get_commits(repository, branch="master"):
+
100
    """ Returns all commits of the given repository.
+
101
    If branch is unspecified, the commits of the master branch are returned.
+
102
    """
+
103
    return repository.iter_commits(branch)
+
104

templates/gitar/directory.html

67 additions and 16 deletions.

View changes Hide changes
1
1
2
2
{% block title %}Gitar | Index page{% endblock title %}
3
-
{% block description %}
+
3
{% block description %}
4
4
Maarten's Gitar app. Still under heavy development, but maintained with <3
5
-
{% endblock description %}
+
5
{% endblock description %}
6
6
{% block main %}
7
7
{% with mdac=materialDesign_accentColor %} {# You'll see why this is handy shortly. #}
8
8
<div class="section {{ materialDesign_color }} lighten-2">
9
9
	<p class="flow-text container white-text">
10
-
		Gitar is a simple web app to easily host Git repositories using the Django framework.
11
-
	</p>
12
-
</div>
13
10
<div class="container section">
14
-
15
-
	<ul>
16
-
	{% for subdirectory in subdirectories %}
17
-
	<li><a href="{{ request.path }}/{{ subdirectory }}">{{ subdirectory }}</a></li>
18
-
	{% endfor %}
19
-
	{% for file in files %}
20
-
	<li><a href="{{ request.path }}{{ file }}">{{ file }}</a></li>
21
-
	{% endfor %}
22
-
	</ul>
23
-
	</div>
24
-
{% endwith %}
+
11
    <div class="row">
+
12
        <div class="col hide-on-med-and-down l4">
+
13
            <!-- Add tertiary information such as branches and licensing here.  -->
+
14
            <h3 class="{{mdc}}-text">{{repository-name}}</h3>
+
15
            <h5 class="{{mdc}}-text">{% trans "Description" %}</h5>
+
16
            {{repository-description}}
+
17
            <h5 class="{{mdc}}-text">Branches</h5>
+
18
            {% for branch in branches %}
+
19
            <a class="{{mdac}}-text text-accent-3" href="{% url 'gitar-repository' repository-name branch %}">
+
20
                {{branch}}
+
21
            </a><br />
+
22
            {% endfor %}
+
23
            <h5 class="{{mdc}}-text">{% trans "Extra information" %}</h5>
+
24
            <div class="chip">
+
25
                {{repository-language}}
+
26
                <i class="material-icons {{mdac}}-text text-accent-3">code</i>
+
27
            </div><br />
+
28
            <div class="chip">
+
29
                {{repository-license}}
+
30
                <i class="material-icons {{mdac}}-text text-accent-3">copyright</i>
+
31
            </div><br />
+
32
        </div>
+
33
        <div class="col s12 m8 l5">
+
34
            <!-- Main area with links to files and subdirectories -->
+
35
            <table class="highlight">
+
36
                {% if subdirectories %}
+
37
                <thead>
+
38
                    {% for subdirectory in subdirectories %}
+
39
                    <a href="{% url 'gitar-repository' repository-name branch subdirectory.path %}">
+
40
                        <tr>
+
41
                            <th>{{subdirectory.name}}</th>
+
42
                        </tr>
+
43
                    </a>
+
44
                    {% endfor %}
+
45
                </thead>
+
46
                {% endif %}
+
47
                <tbody>
+
48
                    {% for file in files %}
+
49
                    <a href="{% url 'gitar-repository' repository-name branch file.path %}">
+
50
                        <tr>
+
51
                            <td>{{file.name}}</td>
+
52
                            <td>{{file.commit|truncatechars:6}}</td>
+
53
                        </tr>
+
54
                    </a>
+
55
                    {% endfor %}
+
56
                </tbody>
+
57
            </table>
+
58
        </div>
+
59
        <div class="col hide-on-small-only m4 l3">
+
60
            <!-- List of commits on the current branch, chronologically. -->
+
61
            <h3 class="{{mdac}}-text text-accent-3">Commits</h3>
+
62
            {% for commit in commits %}
+
63
            <hr />
+
64
            <a class="{{mdac}}-text text-accent-3" href="{% url 'gitar-commit' repository-name commit.hash">
+
65
                {{commit.hash|truncatechars:8}}
+
66
            </a>
+
67
            <span class="{{mdc}}-text text-lighten-2">
+
68
                {% trans "by" %} {{commit.author}}
+
69
            </span><br />
+
70
            {{commit.description|lower|capfirst}}{% if commit.description|last != "." %}.{% endif %}
+
71
            {% endfor %}
+
72
        </div>
+
73
    </div>
+
74
</div>
+
75
{% endwith %}
25
76
{% endblock main %}
26
77

templates/gitar/file.html

3 additions and 2 deletions.

View changes Hide changes
1
1
2
2
{% block title %}Gitar | Index page{% endblock title %}
3
3
4
4
{% block stylesheets %}
5
5
<link href="/static/website/materialize/css/google-icons.css" rel="stylesheet" />
6
6
<link href="/static/website/materialize/css/materialize.css"
7
-
	rel="stylesheet" media="screen, projection" />
8
-
<!--<style>
+
7
<link rel="stylesheet" href="https://cdn.rawgit.com/tonsky/FiraCode/1.204/distr/fira_code.css">
+
8
<!-- TODO: Download Fira Code stylesheet and serve via the static folder. -->
+
9
<!--<style>
9
10
td {
10
11
	padding: 0 0 0 0;
11
12
}
12
13
pre {
13
14
	margin-top: 10px;
14
15
	margin-bottom: 10px;
15
16
}
16
17
table {
17
18
	line-height: 0px;
18
19
	padding-top: 0px;
19
20
	padding-bottom: 0px;
20
21
	border-spacing: 0 0;
21
22
	margin-top: 0px;
22
23
	margin-bottom: 0px;
23
24
}
24
25
</style>-->
25
26
{# For the syntax coloring of Gitar. TODO for later. #}
26
27
<link rel="stylesheet" type="text/css" href="/static/website/syntax.css" />
27
28
<link rel="stylesheet" type="text/css" href="/static/gitar/css/file.css" />
28
29
{% endblock stylesheets %}
29
30
30
31
{% block description %}
31
32
Vngngdn's Gitar app. Really nothing more to say, except that it FREAKING ROCKS!
32
33
{% endblock description %}
33
34
{% block main %}
34
35
{% with mdac=materialDesign_accentColor %} {# You'll see why this is handy shortly. #}
35
36
<div class="section {{ materialDesign_color }} lighten-2">
36
37
	<p class="flow-text container white-text">
37
38
		Gitar is a simple web app to easily host Git repositories using the Django framework.
38
39
	</p>
39
40
</div>
40
41
<div class="container section">
41
42
42
43
	<table>
43
44
		{% for line in content %}
44
45
		<tr>
45
46
			<td id="{{ forloop.counter }}" class="line-number"><pre>{{ forloop.counter }}</pre></td>
46
47
			<td><pre>{{ line|safe }}</pre></td>
47
48
		</tr>
48
49
		{% endfor %}
49
50
	</table>
50
51
{% endwith %}
51
52
{% endblock main %}
52
53

urls.py

9 additions and 2 deletions.

View changes Hide changes
1
1
    Copyright © 2016 Maarten "Vngngdn" Vangeneugden
2
2
3
3
    This program is free software: you can redistribute it and/or modify
4
4
    it under the terms of the GNU Affero General Public License as
5
5
    published by the Free Software Foundation, either version 3 of the
6
6
    License, or (at your option) any later version.
7
7
8
8
    This program is distributed in the hope that it will be useful,
9
9
    but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
    GNU Affero General Public License for more details.
12
12
13
13
    You should have received a copy of the GNU Affero General Public License
14
14
    along with this program. If not, see https://www.gnu.org/licenses/agpl.html.
15
15
"""
16
16
17
17
from django.conf.urls import url
18
18
19
19
from . import views # Imports the views from the same directory (which is views.py).
20
20
21
21
urlpatterns = [
22
22
        url(r'^$', views.index, name='gitar-index'),
23
23
        url(r'^(?P<repository_name>\w+)/$', views.repositories, name='gitar-repository'),
24
-
        url(r'^(?P<repository_name>\w+)/(?P<path>([^/]+/)+)$', views.path_explorer, name='gitar-path-explorer'),
25
-
+
24
        url(r'^(?P<repository_name>\w+)/(?P<commit>[0-9a-f]{20})$', views.commit, name='gitar-commit'),
+
25
        url(r'^(?P<repository_name>\w+)/(?P<branch>\w+)$', views.repositories, name='gitar-repository'),
+
26
        url(r'^(?P<repository_name>\w+)/(?P<branch>\w+)/(?P<path>([^/]+/)+)$', views.path_explorer, name='gitar-path-explorer'),
+
27
26
28
        ]
27
29
+
30
# named as if R2-D2 would. For example: a hexadecimal string of 20 characters
+
31
# is most likely a commit hash, but is a perfectly valid branch name, as well
+
32
# as a file AND directory name. But the chance of someone naming his/her branch
+
33
# like that is so unlikely I'd rather have it this way for now.
+
34

views.py

51 additions and 23 deletions.

View changes Hide changes
1
1
    Copyright © 2016 Maarten "Vngngdn" Vangeneugden
2
2
3
3
    This program is free software: you can redistribute it and/or modify
4
4
    it under the terms of the GNU Affero General Public License as
5
5
    published by the Free Software Foundation, either version 3 of the
6
6
    License, or (at your option) any later version.
7
7
8
8
    This program is distributed in the hope that it will be useful,
9
9
    but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
    GNU Affero General Public License for more details.
12
12
13
13
    You should have received a copy of the GNU Affero General Public License
14
14
    along with this program. If not, see https://www.gnu.org/licenses/agpl.html.
15
15
"""
16
16
17
17
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.
18
18
from django.http import HttpResponseRedirect, HttpResponse 
19
-
from django.core.urlresolvers import reverse 
20
-
from .models import *
+
19
from django.core.urlresolvers import reverse
+
20
from .models import *
21
21
22
22
from .GitActions import RepoInfo
23
23
24
24
from git import Repo  # GitPython functionality.
25
25
import git
26
26
27
27
from .syntax import *
28
28
29
29
# First, I list some standard variables that are common for most of the sites of this app.
30
30
31
31
def footer_description():
32
32
    return "Gitar is a simple web app that allows its users to easily share Git repos in combination with the Django framework."
33
33
34
34
def footer_links():
35
35
    footer_links = [
36
36
            #['Source', 'OHGODHELPNOTDONEYET'],
37
37
            ['Personal website', reverse('about-index')],
38
38
            ]
39
39
    return footer_links
40
40
41
41
def standard_context():
42
42
    context = {
43
43
            'materialDesign_color': "blue-grey",
44
44
            'materialDesign_accentColor': "green",
45
45
            'navbar_title': "Gitar",
46
46
            'navbar_backArrow': False,
47
47
            'footer_title': "Gitar",
48
48
            'footer_description': footer_description(),
49
49
            'footer_links': footer_links(),
50
50
            }
51
51
    return context
52
52
53
53
# From here, the actual views start.
54
54
def index(request):
55
55
    """ The start page of Gitar.
56
56
57
57
    The goal of this view, is to collect all the available repositories,
58
58
    including some additional information, such as programming language,
59
59
    license, description, ... in order to give a fast overview of the most
60
60
    prominent information.
61
61
    """
62
62
63
63
    # Collecting the available repositories:
64
64
    # Template:
65
65
    template = "gitar/index.html"
66
66
    # Requesting the repositories:
67
67
    modelRepos = Repository.objects.all()
68
68
    # From here, we start collecting info about all the repositories:
69
69
    class BlankRepository: pass  # Blank object in which all data will be collected.
70
70
    repositories = []
71
71
    for modelRepo in modelRepos:
72
72
        repository = BlankRepository()
73
73
        # TODO: Find a way to add all of modelRepo's fields without having to
74
74
        # hardcode them. This is prone to errors and is redundant.
75
75
        repository.name = str(modelRepo)
76
76
        repository.programmingLanguage = modelRepo.programmingLanguage
77
77
        repository.license = modelRepo.license
78
78
        repository.description = RepoInfo.get_description(modelRepo)
79
79
80
80
        #gitRepo = Repo.init(modelRepo.directory(), bare=True)  # Connects to the Git Repo.
81
81
        # See tests.py, which assures all repositories exist. Tests are handy.
82
82
        #repository.description = gitRepo.description
83
83
        # This is mostly personal taste, but I like to show the amount of files.
84
84
        #repoTree = gitRepo.heads.master.commit.tree
85
85
        #repository.fileCount = len(repoTree.blobs)  # blobs are files.
86
86
        repositories.append(repository)
87
87
    # After that, I extend the standard context with the repositories:
88
88
    context = standard_context()
89
89
    context['repositories'] = repositories
90
90
    # And finally, sending everything back.
91
91
    return render(request, template, context)
92
92
93
93
def repositories(request, repository_name):
94
-
    # A repo's root is a directory by default, so this will automatically return
+
94
    pass  # TODO
+
95
+
96
def repositories(request, repository_name, branch="master"):
+
97
    # A repo's root is a directory by default, so this will automatically return
95
98
    # a directory view. But still, this is a bit nicer.
96
99
    return path_explorer(request, repository_name, "")
97
-
+
100
98
101
def path_explorer(request, repository_name, path):
99
-
    """ Checks whether the given path is a file or a directory, and calls the
+
102
    """ Checks whether the given path is a file or a directory, and calls the
100
103
    appropriate view function accordingly.
101
104
    """
102
105
    repository = RepoInfo.get_repository_object(repository_name)
103
106
    # From the GitPython documentation:
104
107
    # You can obtain the tree object of a repository, which is the directory of
105
108
    # that repo. This tree can be accessed as if it were a native Python list,
106
109
    # where the elements are the subdirectories and files. So, the idea to
107
110
    # determine whether a file, or a directory was requested, is simple:
108
111
    # 1. Split the path with "/" as seperator.
109
112
    # 2. Replace the current tree variable with the one retrieved from the
110
113
    # subtree element
111
114
    # 3. Repeat 2. until all parts of the given path are exhausted.
112
115
    # If we now still have a tree, we're looking at a directory, so display the
113
116
    # files (and subdirectories) of this directory.
114
117
    # Else, if we hit a blob, display the file contents.
115
118
    path_parts = path.split(sep="/")
116
119
    # FIXME: This is a bug at the URL regex part that I haven't been able to fix
117
120
    # yet. This serves as a temporary fix:
118
121
    # If the last part of the path is an empty string (which happens when the
119
122
    # last symbol was a '/'), remove that part from the list.
120
123
    # Of course, this is bad monkeypatching, but I suck at regex, so as long as
121
124
    # I don't find the solution, this'll have to do.
122
125
123
126
124
127
    print(path_parts)
125
128
126
129
    if path_parts[len(path_parts)-1] == "":
127
130
        path_parts.pop()
128
131
129
132
    if len(path_parts) == 0:
130
133
        directory = repository.heads.master.commit.tree
131
-
        return directory_view(request, repository_name, path, directory)
+
134
        return directory_view(request, repository_name, path, directory)
132
135
133
136
    assert len(path_parts) != 0
134
137
135
138
    # FIXME: If the user gives a "<something>/../<somethingElse>", that should
136
139
    # become "<something>". Obviously, although I think that's done by default
137
140
    # already.
138
141
    directory = repository.heads.master.commit.tree
139
-
    for i in range(len(path_parts)):
+
142
    for i in range(len(path_parts)):
140
143
        subdirectories = directory.trees
141
144
        #if len(subdirectories) == 0:
142
145
            # This can't happen, as this would imply there is a directory inside
143
146
            # a file.
144
147
        #    assert False
145
148
        #else:
146
149
        for subdirectory in subdirectories:
147
150
            if subdirectory.name == path_parts[i]:
148
151
                directory = subdirectory
149
152
                #break  # Useless optimization
150
153
    # When there are no more directories to traverse, check if the last part of
151
154
    # the path is either a file, or a directory:
152
155
    blobs = directory.blobs
153
156
    print(path_parts)
154
157
    last_part = path_parts[len(path_parts)-1]
155
158
    for blob in directory.blobs:
156
159
        print(blob.name)
157
160
        if blob.name == last_part:
158
161
            file_blob = blob
159
162
            print("Returning file view")
160
163
            return file_view(request, repository_name, path, file_blob)
161
-
        else:
+
164
        else:
162
165
            print("blob name: " + blob.name)
163
166
            print("last part: " + last_part)
164
167
    return directory_view(request, repository_name, path, directory)
165
-
+
168
166
169
def directory_view(request, repository_name, path, directory):
167
-
    """ Collects the given directories's files and subdirectories, and renders a
+
170
    """ Collects the given directories's files and subdirectories, and renders a
168
171
    template to display this data.
169
172
    """
170
173
    # Collecting all files:
171
-
    files = directory.blobs
172
-
    file_names = []
173
-
    for ffile in files:
174
-
        file_names.append(ffile.name)
175
-
    # Collecting all subdirectories:
176
-
    subdirectories = directory.trees
177
-
    subdirectory_names = []
178
-
    for subdirectory in subdirectories:
179
-
        subdirectory_names.append(subdirectory.name)
180
-
    # Collecting rendering information:
+
174
    # Collecting files in this directory
+
175
    repository = RepoInfo.get_repository_object(repository_name)
+
176
    files = []
+
177
    for file in directory.blobs:
+
178
        files.append({
+
179
            name:file.name,
+
180
            path:file.path,
+
181
            commit:FileInfo.last_commit(repository, file).binsha,
+
182
            })
+
183
    # Collecting commits for this branch
+
184
    commits = []
+
185
    for commit in repository.iter_commits(branch):
+
186
        commits.append({
+
187
            hash:commit.binsha,
+
188
            author:commit.author,
+
189
            description:commit.summary,
+
190
            })
+
191
    # Collecting subdirectories
+
192
    subdirectories = []
+
193
    for subdirectory in directory.trees:
+
194
        subdirectories.append({
+
195
            path:subdirectory.path,
+
196
            name:subdirectory.name,
+
197
            })
+
198
    # Collecting rendering information:
181
199
    template = "gitar/directory.html"
182
200
    context = standard_context()
183
201
    context["files"] = file_names
184
-
    context["subdirectories"] = subdirectory_names
185
-
    return render(request, template, context)
+
202
    context["subdirectories"] = subdirectories
+
203
    context["commits"] = commits
+
204
    context["branch"] = branch
+
205
    context["repository-name"] = repository_name
+
206
    context["repository-description"] = repository.description
+
207
    # Collection repo information
+
208
    for repo in Repository.objects.all():
+
209
        if str(repo) == repository_name:
+
210
            context["repository-language"] = repo.programmingLanguage
+
211
            context["repository-license"] = repo.license
+
212
            break
+
213
    return render(request, template, context)
186
214
187
215
188
216
def file_view(request, repository_name, path, ffile):
189
-
    """ Collects the file contents of the given file path, and returns it to the
+
217
    """ Collects the file contents of the given file path, and returns it to the
190
218
    template, with the file contents already formatted in HTML using Pygments.
191
219
    """
192
220
193
221
    # Turning the file's contents in HTML ready output:
194
222
    raw_file_data = ffile.data_stream.read()
195
223
    html_code = code_to_HTML(raw_file_data, ffile.name)
196
224
    # Collecting rendering information:
197
225
    template = "gitar/file.html"
198
226
    context = standard_context()
199
227
    context["content"] = html_code
200
228
    return render(request, template, context)
201
229
    
202
230