gitdocs 0.3.6 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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" }
|