gitdocs 0.3.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +15 -14
- data/gitdocs.gemspec +1 -1
- data/lib/gitdocs.rb +2 -1
- data/lib/gitdocs/configuration.rb +1 -1
- data/lib/gitdocs/docfile.rb +37 -0
- data/lib/gitdocs/manager.rb +23 -31
- data/lib/gitdocs/migration/005_add_start_web_frontend.rb +9 -0
- data/lib/gitdocs/public/css/app.css +29 -64
- data/lib/gitdocs/public/css/bootstrap.css +356 -0
- data/lib/gitdocs/public/js/app.js +13 -1
- data/lib/gitdocs/public/js/util.js +86 -0
- data/lib/gitdocs/runner.rb +52 -17
- data/lib/gitdocs/server.rb +9 -7
- data/lib/gitdocs/version.rb +1 -1
- data/lib/gitdocs/views/_header.haml +1 -8
- data/lib/gitdocs/views/app.haml +20 -11
- data/lib/gitdocs/views/dir.haml +25 -15
- data/lib/gitdocs/views/edit.haml +3 -3
- data/lib/gitdocs/views/file.haml +1 -1
- data/lib/gitdocs/views/home.haml +6 -2
- data/lib/gitdocs/views/settings.haml +21 -25
- data/test/test_helper.rb +1 -1
- metadata +8 -8
- data/lib/gitdocs/public/css/reset.css +0 -59
@@ -11,16 +11,28 @@ GitDocs = {
|
|
11
11
|
fullPath = fullPath.replace(subpath + "/", link);
|
12
12
|
});
|
13
13
|
$('span.path').html(fullPath);
|
14
|
+
},
|
15
|
+
// fills in directory meta author and modified for every file
|
16
|
+
fillDirMeta : function(){
|
17
|
+
$('table.listing tbody tr').each(function(i, e) {
|
18
|
+
var file = $(e).find('a').attr('href');
|
19
|
+
$.getJSON(file + "?mode=meta", function(data) {
|
20
|
+
$(e).find('td.author').html(data.author);
|
21
|
+
$(e).find('td.modified').html(RelativeDate.time_ago_in_words(data.modified));
|
22
|
+
});
|
23
|
+
});
|
14
24
|
}
|
15
25
|
};
|
16
26
|
|
17
27
|
$(document).ready(function() {
|
18
28
|
GitDocs.linkBreadcrumbs();
|
29
|
+
GitDocs.fillDirMeta();
|
19
30
|
});
|
20
31
|
|
32
|
+
// Redirect to edit page for new file when new file form is submitted
|
21
33
|
$('form.add').live('submit', function(e){
|
22
34
|
var docIdx = window.location.pathname.match(/(\d+)\//);
|
23
35
|
var fullPath = $('span.path').text();
|
24
36
|
window.location = "/" + docIdx[1] + fullPath + "/" + $(this).find('input.edit').val();
|
25
37
|
e.preventDefault();
|
26
|
-
});
|
38
|
+
});
|
@@ -14,4 +14,90 @@ Utils = {
|
|
14
14
|
}
|
15
15
|
return values;
|
16
16
|
}
|
17
|
+
};
|
18
|
+
|
19
|
+
// DATES
|
20
|
+
// RelativeDate.time_ago_in_words(date)
|
21
|
+
var RelativeDate = {
|
22
|
+
time_ago_in_words: function(from) {
|
23
|
+
return RelativeDate.distance_of_time_in_words(new Date, RelativeDate.parseISO8601(from));
|
24
|
+
|
25
|
+
},
|
26
|
+
distance_of_time_in_words: function(to, from) {
|
27
|
+
var distance_in_seconds = ((to - from) / 1000);
|
28
|
+
var distance_in_minutes = Math.floor(distance_in_seconds / 60);
|
29
|
+
|
30
|
+
if (distance_in_minutes <= 0) { return 'less than a minute ago'; }
|
31
|
+
if (distance_in_minutes == 1) { return 'a minute ago'; }
|
32
|
+
if (distance_in_minutes < 45) { return distance_in_minutes + ' minutes ago'; }
|
33
|
+
if (distance_in_minutes < 120) { return '1 hour ago'; }
|
34
|
+
if (distance_in_minutes < 1440) { return Math.floor(distance_in_minutes / 60) + ' hours ago'; }
|
35
|
+
if (distance_in_minutes < 2880) { return '1 day ago'; }
|
36
|
+
if (distance_in_minutes < 43200) { return Math.floor(distance_in_minutes / 1440) + ' days ago'; }
|
37
|
+
if (distance_in_minutes < 86400) { return '1 month ago'; }
|
38
|
+
if (distance_in_minutes < 525960) { return Math.floor(distance_in_minutes / 43200) + ' months ago'; }
|
39
|
+
if (distance_in_minutes < 1051199) { return 'about 1 year ago'; }
|
40
|
+
|
41
|
+
return 'over ' + Math.floor(distance_in_minutes / 525960) + ' years ago';
|
42
|
+
},
|
43
|
+
parseISO8601 : function(str) {
|
44
|
+
// we assume str is a UTC date ending in 'Z'
|
45
|
+
|
46
|
+
var parts = str.split('T'),
|
47
|
+
dateParts = parts[0].split('-'),
|
48
|
+
timeParts = parts[1].split('Z'),
|
49
|
+
timeSubParts = timeParts[0].split(':'),
|
50
|
+
timeSecParts = timeSubParts[2].split('.'),
|
51
|
+
timeHours = Number(timeSubParts[0]),
|
52
|
+
_date = new Date;
|
53
|
+
|
54
|
+
_date.setUTCFullYear(Number(dateParts[0]));
|
55
|
+
_date.setUTCMonth(Number(dateParts[1])-1);
|
56
|
+
_date.setUTCDate(Number(dateParts[2]));
|
57
|
+
_date.setUTCHours(Number(timeHours));
|
58
|
+
_date.setUTCMinutes(Number(timeSubParts[1]));
|
59
|
+
_date.setUTCSeconds(Number(timeSecParts[0]));
|
60
|
+
if (timeSecParts[1]) _date.setUTCMilliseconds(Number(timeSecParts[1]));
|
61
|
+
|
62
|
+
// by using setUTC methods the date has already been converted to local time(?)
|
63
|
+
return _date;
|
64
|
+
},
|
65
|
+
humanize : function(str, shortened) {
|
66
|
+
var parts = str.split('T')[0].split('-')
|
67
|
+
var humDate = new Date;
|
68
|
+
|
69
|
+
humDate.setFullYear(Number(parts[0]));
|
70
|
+
humDate.setMonth(Number(parts[1])-1);
|
71
|
+
humDate.setDate(Number(parts[2]));
|
72
|
+
|
73
|
+
switch(humDate.getDay())
|
74
|
+
{
|
75
|
+
case 0:
|
76
|
+
var day = "Sunday";
|
77
|
+
break;
|
78
|
+
case 1:
|
79
|
+
var day = "Monday";
|
80
|
+
break;
|
81
|
+
case 2:
|
82
|
+
var day = "Tuesday";
|
83
|
+
break;
|
84
|
+
case 3:
|
85
|
+
var day = "Wednesday";
|
86
|
+
break;
|
87
|
+
case 4:
|
88
|
+
var day = "Thursday";
|
89
|
+
break;
|
90
|
+
case 5:
|
91
|
+
var day = "Friday";
|
92
|
+
break;
|
93
|
+
case 6:
|
94
|
+
var day = "Saturday";
|
95
|
+
break;
|
96
|
+
}
|
97
|
+
if(shortened) {
|
98
|
+
return humDate.toLocaleDateString();
|
99
|
+
} else {
|
100
|
+
return day + ', ' + humDate.toLocaleDateString();
|
101
|
+
}
|
102
|
+
}
|
17
103
|
};
|
data/lib/gitdocs/runner.rb
CHANGED
@@ -18,29 +18,42 @@ module Gitdocs
|
|
18
18
|
@current_remote = @share.remote_name
|
19
19
|
@current_branch = @share.branch_name
|
20
20
|
@current_revision = sh_string("git rev-parse HEAD")
|
21
|
+
mutex = Mutex.new
|
21
22
|
|
22
23
|
info("Running gitdocs!", "Running gitdocs in `#{@root}'")
|
23
24
|
|
24
|
-
mutex = Mutex.new
|
25
25
|
# Pull changes from remote repository
|
26
|
-
|
27
|
-
|
26
|
+
syncer = proc do
|
27
|
+
EM.defer(proc do
|
28
28
|
mutex.synchronize { sync_changes }
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
@listener = FSEvent.new
|
35
|
-
@listener.watch(@root) do |directories|
|
36
|
-
directories.uniq!
|
37
|
-
directories.delete_if {|d| d =~ /\/\.git/}
|
38
|
-
unless directories.empty?
|
39
|
-
mutex.synchronize { push_changes }
|
40
|
-
end
|
29
|
+
end, proc do
|
30
|
+
EM.add_timer(@polling_interval) {
|
31
|
+
syncer.call
|
32
|
+
}
|
33
|
+
end)
|
41
34
|
end
|
42
|
-
|
43
|
-
|
35
|
+
syncer.call
|
36
|
+
# Listen for changes in local repository
|
37
|
+
|
38
|
+
EM.defer(proc{
|
39
|
+
listener = Guard::Listener.select_and_init(@root, :watch_all_modifications => true)
|
40
|
+
listener.on_change { |directories|
|
41
|
+
directories.uniq!
|
42
|
+
directories.delete_if {|d| d =~ /\/\.git/}
|
43
|
+
unless directories.empty?
|
44
|
+
EM.next_tick do
|
45
|
+
EM.defer(proc {
|
46
|
+
mutex.synchronize { push_changes }
|
47
|
+
}, proc {} )
|
48
|
+
end
|
49
|
+
end
|
50
|
+
}
|
51
|
+
listener.start
|
52
|
+
}, proc{EM.stop_reactor})
|
53
|
+
end
|
54
|
+
|
55
|
+
def clear_state
|
56
|
+
@state = nil
|
44
57
|
end
|
45
58
|
|
46
59
|
def sync_changes
|
@@ -115,6 +128,28 @@ module Gitdocs
|
|
115
128
|
end
|
116
129
|
end
|
117
130
|
|
131
|
+
IGNORED_FILES = ['.gitignore']
|
132
|
+
# dir_files("some/dir") => [<Docfile>, <Docfile>]
|
133
|
+
def dir_files(dir)
|
134
|
+
dir_path = File.expand_path(dir, @root)
|
135
|
+
files = {}
|
136
|
+
ls_files = sh_string("git ls-files").split("\n").map { |f| Docfile.new(f) }
|
137
|
+
ls_files.select { |f| f.within?(dir, @root) }.each do |f|
|
138
|
+
path = File.expand_path(f.parent, root)
|
139
|
+
files[path] ||= Docdir.new(path)
|
140
|
+
files[path].files << f unless IGNORED_FILES.include?(f.name)
|
141
|
+
end
|
142
|
+
files.keys.each { |f| files[f].parent = files[File.dirname(f)] }
|
143
|
+
files[dir_path]
|
144
|
+
end
|
145
|
+
|
146
|
+
def file_meta(file)
|
147
|
+
file = file.gsub(%r{^/}, '')
|
148
|
+
author, modified = sh_string("git log --format='%aN|%ai' -n1 #{ShellTools.escape(file)}").split("|")
|
149
|
+
modified = Time.parse(modified.sub(' ', 'T')).utc.iso8601
|
150
|
+
{ :author => author, :modified => modified }
|
151
|
+
end
|
152
|
+
|
118
153
|
def valid?
|
119
154
|
out, status = sh_with_code "git status"
|
120
155
|
status.success?
|
data/lib/gitdocs/server.rb
CHANGED
@@ -2,6 +2,7 @@ require 'thin'
|
|
2
2
|
require 'renee'
|
3
3
|
require 'coderay'
|
4
4
|
require 'uri'
|
5
|
+
require 'haml'
|
5
6
|
|
6
7
|
module Gitdocs
|
7
8
|
class Server
|
@@ -17,10 +18,10 @@ module Gitdocs
|
|
17
18
|
use Rack::Static, :urls => ['/css', '/js', '/img', '/doc'], :root => File.expand_path("../public", __FILE__)
|
18
19
|
run Renee {
|
19
20
|
if request.path_info == '/'
|
20
|
-
render! "home", :layout => 'app', :locals => {:
|
21
|
+
render! "home", :layout => 'app', :locals => {:conf => manager.config, :nav_state => "home" }
|
21
22
|
else
|
22
23
|
path 'settings' do
|
23
|
-
get.render! 'settings', :layout => 'app', :locals => {:conf => manager.config}
|
24
|
+
get.render! 'settings', :layout => 'app', :locals => {:conf => manager.config, :nav_state => "settings" }
|
24
25
|
post do
|
25
26
|
shares = manager.config.shares
|
26
27
|
manager.config.global.update_attributes(request.POST['config'])
|
@@ -45,10 +46,11 @@ module Gitdocs
|
|
45
46
|
parent = File.dirname(file_path)
|
46
47
|
parent = '' if parent == '/'
|
47
48
|
parent = nil if parent == '.'
|
48
|
-
locals = {:idx => idx, :parent => parent, :root => gd.root, :file_path => expanded_path}
|
49
|
+
locals = {:idx => idx, :parent => parent, :root => gd.root, :file_path => expanded_path, :nav_state => nil }
|
49
50
|
mode, mime = request.params['mode'], `file -I #{ShellTools.escape(expanded_path)}`.strip
|
50
|
-
|
51
|
-
|
51
|
+
if mode == 'meta' # Meta
|
52
|
+
halt 200, { 'Content-Type' => 'application/json' }, gd.file_meta(file_path).to_json
|
53
|
+
elsif mode == 'save' # Saving
|
52
54
|
File.open(expanded_path, 'w') { |f| f.print request.params['data'] }
|
53
55
|
redirect! "/" + idx.to_s + file_path
|
54
56
|
elsif mode == 'upload' # Uploading
|
@@ -58,8 +60,8 @@ module Gitdocs
|
|
58
60
|
redirect! "/" + idx.to_s + file_path + "/" + filename
|
59
61
|
elsif !File.exist?(expanded_path) # edit for non-existent file
|
60
62
|
render! "edit", :layout => 'app', :locals => locals.merge(:contents => "")
|
61
|
-
elsif File.directory?(expanded_path)
|
62
|
-
contents =
|
63
|
+
elsif File.directory?(expanded_path) # list directory
|
64
|
+
contents = gd.dir_files(expanded_path)
|
63
65
|
render! "dir", :layout => 'app', :locals => locals.merge(:contents => contents)
|
64
66
|
elsif mode == 'delete' # delete file
|
65
67
|
FileUtils.rm(expanded_path)
|
data/lib/gitdocs/version.rb
CHANGED
@@ -1,11 +1,4 @@
|
|
1
|
-
|
2
|
-
%a{ :href => parent.empty? ? "/#{idx}" : "/#{idx}#{parent}", :class => "parent" }
|
3
|
-
↪ Back to parent
|
4
|
-
- else
|
5
|
-
%a{ :href => "/", :class => "parent" }
|
6
|
-
↪ Back to selection
|
7
|
-
|
8
|
-
%h2
|
1
|
+
%h2.path
|
9
2
|
%span.path= request.path_info.empty? ? '/' : request.path_info
|
10
3
|
- if file
|
11
4
|
%a{ :href => "?mode=raw" } (raw)
|
data/lib/gitdocs/views/app.haml
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
%html
|
3
3
|
%head
|
4
4
|
%title Gitdocs #{Gitdocs::VERSION}
|
5
|
-
%link{ :href => "/css/
|
5
|
+
%link{ :href => "/css/bootstrap.css", :rel => "stylesheet"}
|
6
6
|
%link{ :href => "/css/app.css", :rel => "stylesheet" }
|
7
7
|
%link{ :href => "/css/tilt.css", :rel => "stylesheet" }
|
8
8
|
%link{ :href => "/css/coderay.css", :rel => "stylesheet" }
|
@@ -10,14 +10,23 @@
|
|
10
10
|
%script{ :src => "/js/jquery.js", :type => "text/javascript", :charset => "utf-8" }
|
11
11
|
%script{ :src => "/js/app.js", :type => "text/javascript", :charset => "utf-8" }
|
12
12
|
%body
|
13
|
-
.
|
14
|
-
|
15
|
-
|
16
|
-
%
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
13
|
+
#nav.topbar
|
14
|
+
.fill
|
15
|
+
.container
|
16
|
+
%a{:class => "brand", :href => "/"} Gitdocs
|
17
|
+
%ul(class="nav")
|
18
|
+
%li{ :class => ("active" if nav_state == "home") }
|
19
|
+
%a(href = "/") Home
|
20
|
+
%li{ :class => ("active" if nav_state == "settings") }
|
21
|
+
%a(href = "/settings") Settings
|
21
22
|
|
22
|
-
.
|
23
|
-
|
23
|
+
#main.container
|
24
|
+
.content
|
25
|
+
- if @title
|
26
|
+
.page-header
|
27
|
+
%h1= @title
|
28
|
+
.row
|
29
|
+
.span16= preserve(yield)
|
30
|
+
|
31
|
+
%footer
|
32
|
+
%p © Gitdocs v#{Gitdocs::VERSION}
|
data/lib/gitdocs/views/dir.haml
CHANGED
@@ -1,21 +1,31 @@
|
|
1
|
-
|
1
|
+
- @title = root
|
2
2
|
|
3
3
|
= partial("header", :locals => { :parent => parent, :file => false, :idx => idx })
|
4
4
|
|
5
|
-
%table
|
6
|
-
|
5
|
+
%table.condensed-table.zebra-striped.listing
|
6
|
+
%thead
|
7
7
|
%tr
|
8
|
-
%
|
9
|
-
|
10
|
-
|
8
|
+
%th File
|
9
|
+
%th Author
|
10
|
+
%th Last Modified
|
11
11
|
|
12
|
+
%tbody
|
13
|
+
- contents.items.each_with_index do |f, i|
|
14
|
+
%tr
|
15
|
+
%td
|
16
|
+
%a{ :href => "/#{idx}#{request.path_info}/#{f.name}" }
|
17
|
+
= f.name
|
18
|
+
%td.author
|
19
|
+
%td.modified
|
12
20
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
%
|
19
|
-
|
20
|
-
|
21
|
-
|
21
|
+
.row
|
22
|
+
.span6
|
23
|
+
%form.add
|
24
|
+
%p Add a file in this directory
|
25
|
+
%input{:type => 'text', :name => "path", :class => "edit" }
|
26
|
+
%input{:type => 'submit', :value => "New file", :class => "btn secondary" }
|
27
|
+
.span6
|
28
|
+
%form.upload{ :method => "post", :enctype => "multipart/form-data", :action => "/#{idx}#{request.path_info}?mode=upload" }
|
29
|
+
%p Upload a file to this directory
|
30
|
+
%input{:type => 'file', :value => "Select a file", :name => "file", :class => "uploader" }
|
31
|
+
%input{:type => 'submit', :value => "Upload file", :class => "btn secondary" }
|
data/lib/gitdocs/views/edit.haml
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
|
1
|
+
- @title = root
|
2
2
|
|
3
3
|
= partial("header", :locals => { :parent => parent, :file => true, :idx => idx })
|
4
4
|
|
5
5
|
%form{ :class => "edit", :action => "/#{idx}#{request.path_info}?mode=save", :method => "post", :style => "display:none;" }
|
6
6
|
#editor
|
7
7
|
%textarea{ :id => 'data', :name => "data" }= preserve contents
|
8
|
-
%input{ :type => 'submit', :value => "Save" }
|
9
|
-
%a{ :href => "/#{idx}#{request.path_info}" } Cancel
|
8
|
+
%input{ :type => 'submit', :value => "Save", :class => "btn primary" }
|
9
|
+
%a{ :href => "/#{idx}#{request.path_info}", :class => "btn secondary" } Cancel
|
10
10
|
%input{ :type => 'hidden', :class => 'filename', :value => request.path_info }
|
11
11
|
|
12
12
|
= partial("ace_scripts")
|
data/lib/gitdocs/views/file.haml
CHANGED
data/lib/gitdocs/views/home.haml
CHANGED
@@ -1,35 +1,33 @@
|
|
1
|
-
|
1
|
+
- @title = "Settings"
|
2
2
|
|
3
3
|
%form{:method => 'POST', :action => '/settings'}
|
4
4
|
%h2 Gitdocs
|
5
|
-
|
6
|
-
%
|
7
|
-
|
8
|
-
|
9
|
-
%input{:type =>'hidden', :value => '0', :name=>"config[load_browser_on_startup]"}
|
10
|
-
%input{:type =>'checkbox', :value => '1', :name=>"config[load_browser_on_startup]", :checked => conf.global.load_browser_on_startup ? 'checked' : nil}
|
11
|
-
|
5
|
+
#config.field.config
|
6
|
+
%input{:type =>'hidden', :value => '0', :name=>"config[load_browser_on_startup]"}
|
7
|
+
%input{:type =>'checkbox', :value => '1', :name=>"config[load_browser_on_startup]", :checked => conf.global.load_browser_on_startup ? 'checked' : nil}
|
8
|
+
%span Open browser on startup?
|
12
9
|
|
13
10
|
%h2 Shares
|
14
|
-
-conf.shares.each_with_index do |share, idx|
|
11
|
+
- conf.shares.each_with_index do |share, idx|
|
15
12
|
%div{:id => "share-#{idx}", :class => "share #{idx % 2 == 0 ? 'even' : 'odd'}"}
|
16
13
|
%dl
|
17
14
|
%dt Path
|
18
15
|
%dd
|
19
|
-
%input{:name=>"share[#{idx}][path]", :value => share.path}
|
16
|
+
%input{:name=>"share[#{idx}][path]", :value => share.path, :class => "path" }
|
20
17
|
%dl
|
21
18
|
%dt Polling interval
|
22
19
|
%dd
|
23
20
|
%input{:name=>"share[#{idx}][polling_interval]", :value => share.polling_interval}
|
24
|
-
|
21
|
+
|
22
|
+
- if share.available_remotes
|
25
23
|
%dl
|
26
24
|
%dt Remote
|
27
25
|
%dd
|
28
26
|
%select{:name=>"share[#{idx}][remote_branch]"}
|
29
|
-
-share.available_remotes.each do |remote|
|
27
|
+
- share.available_remotes.each do |remote|
|
30
28
|
%option{:value => remote, :selected => remote == "#{share.remote_name}/#{share.branch_name}" ? 'selected' : nil}
|
31
|
-
=remote
|
32
|
-
-else
|
29
|
+
= remote
|
30
|
+
- else
|
33
31
|
|
34
32
|
%dl
|
35
33
|
%dt Remote
|
@@ -39,14 +37,12 @@
|
|
39
37
|
%dt Branch
|
40
38
|
%dd
|
41
39
|
%input{:name=>"share[#{idx}][branch_name]", :value => share.branch_name}
|
42
|
-
|
43
|
-
%
|
44
|
-
%
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
%
|
49
|
-
|
50
|
-
|
51
|
-
%input{:type =>'hidden', :name=>"share[#{idx}][delete]"}
|
52
|
-
%input{:value => 'Save', :type => 'submit'}
|
40
|
+
.notify.field
|
41
|
+
%input{:type =>'hidden', :value => '0', :name=>"share[#{idx}][notification]"}
|
42
|
+
%input{:type =>'checkbox', :value => '1', :name=>"share[#{idx}][notification]", :checked => share.notification ? 'checked' : nil}
|
43
|
+
%span Notifications?
|
44
|
+
.delete
|
45
|
+
%input{:type =>'button', :value => "Delete", :class => "btn danger"}
|
46
|
+
%input{:type =>'hidden', :name=>"share[#{idx}][delete]"}
|
47
|
+
|
48
|
+
%input{:value => 'Save', :type => 'submit', :class => "btn primary" }
|