siuying-gitdocs 0.4.14.md
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/.gitignore +5 -0
- data/CHANGELOG +79 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +192 -0
- data/Rakefile +8 -0
- data/bin/gitdocs +5 -0
- data/gitdocs.gemspec +41 -0
- data/lib/gitdocs/cli.rb +111 -0
- data/lib/gitdocs/configuration.rb +79 -0
- data/lib/gitdocs/docfile.rb +23 -0
- data/lib/gitdocs/manager.rb +96 -0
- data/lib/gitdocs/markdown_converter.rb +44 -0
- data/lib/gitdocs/markdown_template.rb +20 -0
- data/lib/gitdocs/migration/001_create_shares.rb +13 -0
- data/lib/gitdocs/migration/002_add_remote_branch.rb +10 -0
- data/lib/gitdocs/migration/003_create_configs.rb +11 -0
- data/lib/gitdocs/migration/004_add_index_for_path.rb +10 -0
- data/lib/gitdocs/migration/005_add_start_web_frontend.rb +9 -0
- data/lib/gitdocs/migration/006_add_web_port_to_config.rb +9 -0
- data/lib/gitdocs/public/css/app.css +51 -0
- data/lib/gitdocs/public/css/bootstrap.css +356 -0
- data/lib/gitdocs/public/css/coderay.css +41 -0
- data/lib/gitdocs/public/css/tilt.css +82 -0
- data/lib/gitdocs/public/img/error.png +0 -0
- data/lib/gitdocs/public/img/file.png +0 -0
- data/lib/gitdocs/public/img/folder.png +0 -0
- data/lib/gitdocs/public/img/git_logo.png +0 -0
- data/lib/gitdocs/public/img/info.png +0 -0
- data/lib/gitdocs/public/img/ok.png +0 -0
- data/lib/gitdocs/public/img/warning.png +0 -0
- data/lib/gitdocs/public/js/ace/ace-compat.js +1 -0
- data/lib/gitdocs/public/js/ace/ace-uncompressed.js +14202 -0
- data/lib/gitdocs/public/js/ace/ace.js +1 -0
- data/lib/gitdocs/public/js/ace/keybinding-emacs.js +1 -0
- data/lib/gitdocs/public/js/ace/keybinding-vim.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-coffee.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-css.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-html.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-javascript.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-json.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-lua.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-markdown.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-php.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-python.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-ruby.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-scala.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-scss.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-sql.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-svg.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-textile.js +1 -0
- data/lib/gitdocs/public/js/ace/mode-xml.js +1 -0
- data/lib/gitdocs/public/js/ace/theme-tomorrow.js +1 -0
- data/lib/gitdocs/public/js/ace/theme-tomorrow_night.js +1 -0
- data/lib/gitdocs/public/js/ace/theme-tomorrow_night_blue.js +1 -0
- data/lib/gitdocs/public/js/ace/theme-twilight.js +1 -0
- data/lib/gitdocs/public/js/ace/theme-vibrant_ink.js +1 -0
- data/lib/gitdocs/public/js/ace/worker-coffee.js +7041 -0
- data/lib/gitdocs/public/js/ace/worker-css.js +9525 -0
- data/lib/gitdocs/public/js/ace/worker-javascript.js +9739 -0
- data/lib/gitdocs/public/js/app.js +107 -0
- data/lib/gitdocs/public/js/bootstrap-alerts.js +113 -0
- data/lib/gitdocs/public/js/edit.js +30 -0
- data/lib/gitdocs/public/js/jquery.js +4 -0
- data/lib/gitdocs/public/js/jquery.tablesorter.js +4 -0
- data/lib/gitdocs/public/js/search.js +13 -0
- data/lib/gitdocs/public/js/settings.js +25 -0
- data/lib/gitdocs/public/js/util.js +145 -0
- data/lib/gitdocs/runner.rb +259 -0
- data/lib/gitdocs/server.rb +154 -0
- data/lib/gitdocs/version.rb +3 -0
- data/lib/gitdocs/views/_ace_scripts.erb +8 -0
- data/lib/gitdocs/views/_header.haml +14 -0
- data/lib/gitdocs/views/app.haml +40 -0
- data/lib/gitdocs/views/dir.haml +43 -0
- data/lib/gitdocs/views/edit.haml +15 -0
- data/lib/gitdocs/views/file.haml +8 -0
- data/lib/gitdocs/views/home.haml +10 -0
- data/lib/gitdocs/views/revisions.haml +28 -0
- data/lib/gitdocs/views/search.haml +16 -0
- data/lib/gitdocs/views/settings.haml +55 -0
- data/lib/gitdocs.rb +37 -0
- data/lib/img/icon.png +0 -0
- data/test/configuration_test.rb +39 -0
- data/test/runner_test.rb +25 -0
- data/test/test_helper.rb +54 -0
- metadata +370 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Utils = {
|
|
2
|
+
getKeys : function(hash) {
|
|
3
|
+
var keys = [];
|
|
4
|
+
for(var i in hash) {
|
|
5
|
+
keys.push(i);
|
|
6
|
+
}
|
|
7
|
+
return keys;
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
getValues : function(hash) {
|
|
11
|
+
var values = [];
|
|
12
|
+
for(var i in hash) {
|
|
13
|
+
values.push(hash[i]);
|
|
14
|
+
}
|
|
15
|
+
return values;
|
|
16
|
+
},
|
|
17
|
+
// humanizeBytes(1234)
|
|
18
|
+
humanizeBytes : function(filesize) {
|
|
19
|
+
if (filesize == null || filesize <= 0 || filesize == "") { return "—" }
|
|
20
|
+
if (filesize >= 1073741824) {
|
|
21
|
+
filesize = Utils.number_format(filesize / 1073741824, 2, '.', '') + ' Gb';
|
|
22
|
+
} else {
|
|
23
|
+
if (filesize >= 1048576) {
|
|
24
|
+
filesize = Utils.number_format(filesize / 1048576, 2, '.', '') + ' Mb';
|
|
25
|
+
} else {
|
|
26
|
+
if (filesize >= 1024) {
|
|
27
|
+
filesize = Utils.number_format(filesize / 1024, 0) + ' Kb';
|
|
28
|
+
} else {
|
|
29
|
+
filesize = Utils.number_format(filesize, 0) + ' bytes';
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
return filesize;
|
|
34
|
+
},
|
|
35
|
+
number_format : function( number, decimals, dec_point, thousands_sep ) {
|
|
36
|
+
var n = number, c = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals;
|
|
37
|
+
var d = dec_point == undefined ? "," : dec_point;
|
|
38
|
+
var t = thousands_sep == undefined ? "." : thousands_sep, s = n < 0 ? "-" : "";
|
|
39
|
+
var i = parseInt(n = Math.abs(+n || 0).toFixed(c)) + "", j = (j = i.length) > 3 ? j % 3 : 0;
|
|
40
|
+
return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : "");
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Strings
|
|
45
|
+
StringFormatter = {
|
|
46
|
+
// autolink text within a plain text file
|
|
47
|
+
// apply to the wrapper around any text (.autolink)
|
|
48
|
+
autoLink : function() {
|
|
49
|
+
$('.autolink:not(.linked)').each(function(index, item) {
|
|
50
|
+
var result = $(item).html().toString() + ' ';
|
|
51
|
+
$(result.match(/(https?.*?)[^<\s]*/gm)).each(function(index, linkString) {
|
|
52
|
+
var link = "<a href='" + linkString + "' target='_blank'>" + linkString + "</a>";
|
|
53
|
+
result = result.replace(linkString, link);
|
|
54
|
+
$(item).addClass('linked');
|
|
55
|
+
});
|
|
56
|
+
$(item).html(result.slice(0, -1));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// DATES
|
|
62
|
+
// RelativeDate.time_ago_in_words(date)
|
|
63
|
+
var RelativeDate = {
|
|
64
|
+
time_ago_in_words: function(from) {
|
|
65
|
+
return RelativeDate.distance_of_time_in_words(new Date, RelativeDate.parseISO8601(from));
|
|
66
|
+
|
|
67
|
+
},
|
|
68
|
+
distance_of_time_in_words: function(to, from) {
|
|
69
|
+
var distance_in_seconds = ((to - from) / 1000);
|
|
70
|
+
var distance_in_minutes = Math.floor(distance_in_seconds / 60);
|
|
71
|
+
|
|
72
|
+
if (distance_in_minutes <= 0) { return 'less than a minute ago'; }
|
|
73
|
+
if (distance_in_minutes == 1) { return 'a minute ago'; }
|
|
74
|
+
if (distance_in_minutes < 45) { return distance_in_minutes + ' minutes ago'; }
|
|
75
|
+
if (distance_in_minutes < 120) { return '1 hour ago'; }
|
|
76
|
+
if (distance_in_minutes < 1440) { return Math.floor(distance_in_minutes / 60) + ' hours ago'; }
|
|
77
|
+
if (distance_in_minutes < 2880) { return '1 day ago'; }
|
|
78
|
+
if (distance_in_minutes < 43200) { return Math.floor(distance_in_minutes / 1440) + ' days ago'; }
|
|
79
|
+
if (distance_in_minutes < 86400) { return '1 month ago'; }
|
|
80
|
+
if (distance_in_minutes < 525960) { return Math.floor(distance_in_minutes / 43200) + ' months ago'; }
|
|
81
|
+
if (distance_in_minutes < 1051199) { return 'about 1 year ago'; }
|
|
82
|
+
|
|
83
|
+
return 'over ' + Math.floor(distance_in_minutes / 525960) + ' years ago';
|
|
84
|
+
},
|
|
85
|
+
parseISO8601 : function(str) {
|
|
86
|
+
// we assume str is a UTC date ending in 'Z'
|
|
87
|
+
|
|
88
|
+
var parts = str.split('T'),
|
|
89
|
+
dateParts = parts[0].split('-'),
|
|
90
|
+
timeParts = parts[1].split('Z'),
|
|
91
|
+
timeSubParts = timeParts[0].split(':'),
|
|
92
|
+
timeSecParts = timeSubParts[2].split('.'),
|
|
93
|
+
timeHours = Number(timeSubParts[0]),
|
|
94
|
+
_date = new Date;
|
|
95
|
+
|
|
96
|
+
_date.setUTCFullYear(Number(dateParts[0]));
|
|
97
|
+
_date.setUTCMonth(Number(dateParts[1])-1);
|
|
98
|
+
_date.setUTCDate(Number(dateParts[2]));
|
|
99
|
+
_date.setUTCHours(Number(timeHours));
|
|
100
|
+
_date.setUTCMinutes(Number(timeSubParts[1]));
|
|
101
|
+
_date.setUTCSeconds(Number(timeSecParts[0]));
|
|
102
|
+
if (timeSecParts[1]) _date.setUTCMilliseconds(Number(timeSecParts[1]));
|
|
103
|
+
|
|
104
|
+
// by using setUTC methods the date has already been converted to local time(?)
|
|
105
|
+
return _date;
|
|
106
|
+
},
|
|
107
|
+
humanize : function(str, shortened) {
|
|
108
|
+
var parts = str.split('T')[0].split('-')
|
|
109
|
+
var humDate = new Date;
|
|
110
|
+
|
|
111
|
+
humDate.setFullYear(Number(parts[0]));
|
|
112
|
+
humDate.setMonth(Number(parts[1])-1);
|
|
113
|
+
humDate.setDate(Number(parts[2]));
|
|
114
|
+
|
|
115
|
+
switch(humDate.getDay())
|
|
116
|
+
{
|
|
117
|
+
case 0:
|
|
118
|
+
var day = "Sunday";
|
|
119
|
+
break;
|
|
120
|
+
case 1:
|
|
121
|
+
var day = "Monday";
|
|
122
|
+
break;
|
|
123
|
+
case 2:
|
|
124
|
+
var day = "Tuesday";
|
|
125
|
+
break;
|
|
126
|
+
case 3:
|
|
127
|
+
var day = "Wednesday";
|
|
128
|
+
break;
|
|
129
|
+
case 4:
|
|
130
|
+
var day = "Thursday";
|
|
131
|
+
break;
|
|
132
|
+
case 5:
|
|
133
|
+
var day = "Friday";
|
|
134
|
+
break;
|
|
135
|
+
case 6:
|
|
136
|
+
var day = "Saturday";
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
if(shortened) {
|
|
140
|
+
return humDate.toLocaleDateString();
|
|
141
|
+
} else {
|
|
142
|
+
return day + ', ' + humDate.toLocaleDateString();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
module Gitdocs
|
|
2
|
+
class Runner
|
|
3
|
+
include ShellTools
|
|
4
|
+
|
|
5
|
+
attr_reader :root, :listener
|
|
6
|
+
|
|
7
|
+
def initialize(share)
|
|
8
|
+
@share = share
|
|
9
|
+
@root = share.path.sub(%r{/+$},'') if share.path
|
|
10
|
+
@polling_interval = share.polling_interval
|
|
11
|
+
@icon = File.expand_path("../../img/icon.png", __FILE__)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
SearchResult = Struct.new(:file, :context)
|
|
15
|
+
def search(term)
|
|
16
|
+
return [] if term.empty?
|
|
17
|
+
|
|
18
|
+
results = []
|
|
19
|
+
if result_test = sh_string("git grep -i #{ShellTools.escape(term)}")
|
|
20
|
+
result_test.scan(/(.*?):([^\n]*)/) do |(file, context)|
|
|
21
|
+
if result = results.find { |s| s.file == file }
|
|
22
|
+
result.context += ' ... ' + context
|
|
23
|
+
else
|
|
24
|
+
results << SearchResult.new(file, context)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
results
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def run
|
|
32
|
+
return false unless self.valid?
|
|
33
|
+
|
|
34
|
+
@show_notifications = @share.notification
|
|
35
|
+
@current_remote = @share.remote_name
|
|
36
|
+
@current_branch = @share.branch_name
|
|
37
|
+
@current_revision = sh_string("git rev-parse HEAD")
|
|
38
|
+
Guard::Notifier.turn_on if @show_notifications
|
|
39
|
+
|
|
40
|
+
mutex = Mutex.new
|
|
41
|
+
|
|
42
|
+
info("Running gitdocs!", "Running gitdocs in `#{@root}'")
|
|
43
|
+
|
|
44
|
+
# Pull changes from remote repository
|
|
45
|
+
syncer = proc do
|
|
46
|
+
EM.defer(proc do
|
|
47
|
+
mutex.synchronize { sync_changes }
|
|
48
|
+
end, proc do
|
|
49
|
+
EM.add_timer(@polling_interval) {
|
|
50
|
+
syncer.call
|
|
51
|
+
}
|
|
52
|
+
end)
|
|
53
|
+
end
|
|
54
|
+
syncer.call
|
|
55
|
+
# Listen for changes in local repository
|
|
56
|
+
|
|
57
|
+
EM.defer(proc{
|
|
58
|
+
listener = Guard::Listener.select_and_init(@root, :watch_all_modifications => true)
|
|
59
|
+
listener.on_change { |directories|
|
|
60
|
+
directories.uniq!
|
|
61
|
+
directories.delete_if {|d| d =~ /\/\.git/}
|
|
62
|
+
unless directories.empty?
|
|
63
|
+
EM.next_tick do
|
|
64
|
+
EM.defer(proc {
|
|
65
|
+
mutex.synchronize { push_changes }
|
|
66
|
+
}, proc {} )
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
}
|
|
70
|
+
listener.start
|
|
71
|
+
}, proc{EM.stop_reactor})
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def clear_state
|
|
75
|
+
@state = nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def sync_changes
|
|
79
|
+
out, status = sh_with_code("git fetch --all && git merge #{@current_remote}/#{@current_branch}")
|
|
80
|
+
if status.success?
|
|
81
|
+
changes = get_latest_changes
|
|
82
|
+
unless changes.empty?
|
|
83
|
+
author_list = changes.inject(Hash.new{|h, k| h[k] = 0}) {|h, c| h[c['author']] += 1; h}.to_a.sort{|a,b| b[1] <=> a[1]}.map{|(name, count)| "* #{name} (#{count} change#{count == 1 ? '' : 's'})"}.join("\n")
|
|
84
|
+
info("Updated with #{changes.size} change#{changes.size == 1 ? '' : 's'}", "In `#{@root}':\n#{author_list}")
|
|
85
|
+
end
|
|
86
|
+
push_changes
|
|
87
|
+
elsif out[/CONFLICT/]
|
|
88
|
+
conflicted_files = sh("git ls-files -u --full-name -z").split("\0").
|
|
89
|
+
inject(Hash.new{|h, k| h[k] = []}) {|h, line|
|
|
90
|
+
parts = line.split(/\t/)
|
|
91
|
+
h[parts.last] << parts.first.split(/ /)
|
|
92
|
+
h
|
|
93
|
+
}
|
|
94
|
+
warn("There were some conflicts", "#{conflicted_files.keys.map{|f| "* #{f}"}.join("\n")}")
|
|
95
|
+
conflicted_files.each do |conflict, ids|
|
|
96
|
+
conflict_start, conflict_end = conflict.scan(/(.*?)(|\.[^\.]+)$/).first
|
|
97
|
+
ids.each do |(mode, sha, id)|
|
|
98
|
+
author = " original" if id == "1"
|
|
99
|
+
system("cd #{@root} && git show :#{id}:#{conflict} > '#{conflict_start} (#{sha[0..6]}#{author})#{conflict_end}'")
|
|
100
|
+
end
|
|
101
|
+
system("cd #{@root} && git rm #{conflict}") or raise
|
|
102
|
+
end
|
|
103
|
+
push_changes
|
|
104
|
+
elsif sh_string("git remote").nil? # no remote to pull from
|
|
105
|
+
# Do nothing, no remote repo yet
|
|
106
|
+
else
|
|
107
|
+
error("There was a problem synchronizing this gitdoc", "A problem occurred in #{@root}:\n#{out}")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def push_changes
|
|
112
|
+
message_file = File.expand_path(".gitmessage~", @root)
|
|
113
|
+
if File.exist? message_file
|
|
114
|
+
message = File.read message_file
|
|
115
|
+
File.delete message_file
|
|
116
|
+
else
|
|
117
|
+
message = 'Auto-commit from gitdocs'
|
|
118
|
+
end
|
|
119
|
+
sh 'find . -type d -regex ``./[^.].*'' -empty -exec touch \'{}/.gitignore\' \;'
|
|
120
|
+
sh 'git add .'
|
|
121
|
+
sh "git commit -a -m #{ShellTools.escape(message)}" unless sh("git status -s").strip.empty?
|
|
122
|
+
if @current_revision.nil? || sh('git status')[/branch is ahead/]
|
|
123
|
+
out, code = sh_with_code("git push #{@current_remote} #{@current_branch}")
|
|
124
|
+
if code.success?
|
|
125
|
+
changes = get_latest_changes
|
|
126
|
+
info("Pushed #{changes.size} change#{changes.size == 1 ? '' : 's'}", "`#{@root}' has been pushed")
|
|
127
|
+
elsif @current_revision.nil?
|
|
128
|
+
# ignorable
|
|
129
|
+
elsif out[/\[rejected\]/]
|
|
130
|
+
warn("There was a conflict in #{@root}, retrying", "")
|
|
131
|
+
else
|
|
132
|
+
error("BAD Could not push changes in #{@root}", out)
|
|
133
|
+
exit
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def get_latest_changes
|
|
139
|
+
if @current_revision
|
|
140
|
+
out = sh "git log #{@current_revision}.. --pretty='format:{\"commit\": \"%H\",%n \"author\": \"%an <%ae>\",%n \"date\": \"%ad\",%n \"message\": \"%s\"%n}'"
|
|
141
|
+
if out.empty?
|
|
142
|
+
[]
|
|
143
|
+
else
|
|
144
|
+
lines = []
|
|
145
|
+
Yajl::Parser.new.parse(out) do |obj|
|
|
146
|
+
lines << obj
|
|
147
|
+
end
|
|
148
|
+
@current_revision = sh("git rev-parse HEAD").strip
|
|
149
|
+
lines
|
|
150
|
+
end
|
|
151
|
+
else
|
|
152
|
+
[]
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
IGNORED_FILES = ['.gitignore']
|
|
157
|
+
# Returns the list of files in a given directory
|
|
158
|
+
# dir_files("some/dir") => [<Docfile>, <Docfile>]
|
|
159
|
+
def dir_files(dir_path)
|
|
160
|
+
Dir[File.join(dir_path, "*")].to_a.map { |path| Docfile.new(path) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Returns file meta data based on relative file path
|
|
164
|
+
# file_meta("path/to/file")
|
|
165
|
+
# => { :author => "Nick", :size => 1000, :modified => ... }
|
|
166
|
+
def file_meta(file)
|
|
167
|
+
result = {}
|
|
168
|
+
file = file.gsub(%r{^/}, '')
|
|
169
|
+
full_path = File.expand_path(file, @root)
|
|
170
|
+
log_result = sh_string("git log --format='%aN|%ai' -n1 #{ShellTools.escape(file)}")
|
|
171
|
+
result = {} unless File.exist?(full_path) && log_result
|
|
172
|
+
author, modified = log_result.split("|")
|
|
173
|
+
modified = Time.parse(modified.sub(' ', 'T')).utc.iso8601
|
|
174
|
+
size = if File.directory?(full_path)
|
|
175
|
+
Dir[File.join(full_path, '**', '*')].inject(0) do |size, file|
|
|
176
|
+
File.symlink?(file) ? size : size += File.size(file)
|
|
177
|
+
end
|
|
178
|
+
else
|
|
179
|
+
File.symlink?(full_path) ? 0 : File.size(full_path)
|
|
180
|
+
end
|
|
181
|
+
size = -1 if size == 0 # A value of 0 breaks the table sort for some reason
|
|
182
|
+
result = { :author => author, :size => size, :modified => modified }
|
|
183
|
+
result
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Returns the revisions available for a particular file
|
|
187
|
+
# file_revisions("README")
|
|
188
|
+
def file_revisions(file)
|
|
189
|
+
file = file.gsub(%r{^/}, '')
|
|
190
|
+
output = sh_string("git log --format='%h|%s|%aN|%ai' -n100 #{ShellTools.escape(file)}")
|
|
191
|
+
output.to_s.split("\n").map do |log_result|
|
|
192
|
+
commit, subject, author, date = log_result.split("|")
|
|
193
|
+
date = Time.parse(date.sub(' ', 'T')).utc.iso8601
|
|
194
|
+
{ :commit => commit, :subject => subject, :author => author, :date => date }
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Returns the temporary path of a particular revision of a file
|
|
199
|
+
# file_revision_at("README", "a4c56h") => "/tmp/some/path/README"
|
|
200
|
+
def file_revision_at(file, ref)
|
|
201
|
+
file = file.gsub(%r{^/}, '')
|
|
202
|
+
content = sh_string("git show #{ref}:#{ShellTools.escape(file)}")
|
|
203
|
+
tmp_path = File.expand_path(File.basename(file), Dir.tmpdir)
|
|
204
|
+
File.open(tmp_path, 'w') { |f| f.puts content }
|
|
205
|
+
tmp_path
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Revert a file to a particular revision
|
|
209
|
+
def file_revert(file, ref)
|
|
210
|
+
if file_revisions(file).map {|r| r[:commit]}.include? ref
|
|
211
|
+
file = file.gsub(%r{^/}, '')
|
|
212
|
+
full_path = File.expand_path(file, @root)
|
|
213
|
+
content = File.read(file_revision_at(file, ref))
|
|
214
|
+
File.open(full_path, 'w') { |f| f.puts content }
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def valid?
|
|
219
|
+
out, status = sh_with_code "git status"
|
|
220
|
+
@root.present? && status.success?
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def warn(title, msg)
|
|
224
|
+
if @show_notifications
|
|
225
|
+
Guard::Notifier.notify(msg, :title => title)
|
|
226
|
+
else
|
|
227
|
+
Kernel.warn("#{title}: #{msg}")
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def info(title, msg)
|
|
232
|
+
if @show_notifications
|
|
233
|
+
Guard::Notifier.notify(msg, :title => title, :image => @icon)
|
|
234
|
+
else
|
|
235
|
+
puts("#{title}: #{msg}")
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def error(title, msg)
|
|
240
|
+
if @show_notifications
|
|
241
|
+
Guard::Notifier.notify(msg, :title => title, :image => :failure)
|
|
242
|
+
else
|
|
243
|
+
Kernel.warn("#{title}: #{msg}")
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# sh_string("git config branch.`git branch | grep '^\*' | sed -e 's/\* //'`.remote", "origin")
|
|
248
|
+
def sh_string(cmd, default=nil)
|
|
249
|
+
val = sh(cmd).strip rescue nil
|
|
250
|
+
(val.nil? || val.empty?) ? default : val
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Run in shell, return both status and output
|
|
254
|
+
# @see #sh
|
|
255
|
+
def sh_with_code(cmd)
|
|
256
|
+
ShellTools.sh_with_code(cmd, @root)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
require 'thin'
|
|
2
|
+
require 'renee'
|
|
3
|
+
require 'coderay'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'haml'
|
|
6
|
+
require 'mimetype_fu'
|
|
7
|
+
require 'pygments'
|
|
8
|
+
|
|
9
|
+
module Gitdocs
|
|
10
|
+
class Server
|
|
11
|
+
def initialize(manager, *gitdocs)
|
|
12
|
+
@manager = manager
|
|
13
|
+
@gitdocs = gitdocs
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def start(port = 8888)
|
|
17
|
+
gds = @gitdocs
|
|
18
|
+
manager = @manager
|
|
19
|
+
|
|
20
|
+
Tilt.prefer ::MarkdownTemplate, 'md'
|
|
21
|
+
Tilt.prefer ::MarkdownTemplate, 'mkd'
|
|
22
|
+
Tilt.prefer ::MarkdownTemplate, 'markdown'
|
|
23
|
+
|
|
24
|
+
Thin::Logging.debug = @manager.debug
|
|
25
|
+
Thin::Server.start('127.0.0.1', port) do
|
|
26
|
+
use Rack::Static, :urls => ['/css', '/js', '/img', '/doc'], :root => File.expand_path("../public", __FILE__)
|
|
27
|
+
use Rack::MethodOverride
|
|
28
|
+
run Renee {
|
|
29
|
+
if request.path_info == '/'
|
|
30
|
+
if manager.config.shares.size == 1
|
|
31
|
+
redirect! "/0"
|
|
32
|
+
else
|
|
33
|
+
render! "home", :layout => 'app', :locals => {:conf => manager.config, :nav_state => "home" }
|
|
34
|
+
end
|
|
35
|
+
else
|
|
36
|
+
path 'settings' do
|
|
37
|
+
get.render! 'settings', :layout => 'app', :locals => {:conf => manager.config, :nav_state => "settings" }
|
|
38
|
+
post do
|
|
39
|
+
shares = manager.config.shares
|
|
40
|
+
manager.config.global.update_attributes(request.POST['config'])
|
|
41
|
+
request.POST['share'].each do |idx, share|
|
|
42
|
+
if remote_branch = share.delete('remote_branch')
|
|
43
|
+
share['remote_name'], share['branch_name'] = remote_branch.split('/', 2)
|
|
44
|
+
end
|
|
45
|
+
# Update paths
|
|
46
|
+
if share['path'] && !share['path'].empty?
|
|
47
|
+
shares[Integer(idx)].update_attributes(share)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
EM.add_timer(0.1) { manager.restart }
|
|
51
|
+
redirect! '/settings'
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
path('search').get do
|
|
56
|
+
render! "search", :layout => 'app', :locals => {:conf => manager.config, :results => manager.search(request.GET['q']), :nav_state => nil}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
path('shares') do
|
|
60
|
+
post do
|
|
61
|
+
Configuration::Share.create
|
|
62
|
+
redirect! '/settings'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
var(:int) do |id|
|
|
66
|
+
delete do
|
|
67
|
+
share = manager.config.shares.find { |s| s.id == id }
|
|
68
|
+
halt 404 if share.nil?
|
|
69
|
+
share.destroy
|
|
70
|
+
redirect! '/settings'
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
var :int do |idx|
|
|
76
|
+
gd = gds[idx]
|
|
77
|
+
halt 404 if gd.nil?
|
|
78
|
+
file_path = URI.unescape(request.path_info)
|
|
79
|
+
file_ext = File.extname(file_path)
|
|
80
|
+
expanded_path = File.expand_path(".#{file_path}", gd.root)
|
|
81
|
+
message_file = File.expand_path(".gitmessage~", gd.root)
|
|
82
|
+
halt 400 unless expanded_path[/^#{Regexp.quote(gd.root)}/]
|
|
83
|
+
parent = File.dirname(file_path)
|
|
84
|
+
parent = '' if parent == '/'
|
|
85
|
+
parent = nil if parent == '.'
|
|
86
|
+
locals = {:idx => idx, :parent => parent, :root => gd.root, :file_path => expanded_path, :nav_state => nil }
|
|
87
|
+
mime = File.mime_type?(File.open(expanded_path)) if File.file?(expanded_path)
|
|
88
|
+
mode = request.params['mode']
|
|
89
|
+
if mode == 'meta' # Meta
|
|
90
|
+
halt 200, { 'Content-Type' => 'application/json' }, [gd.file_meta(file_path).to_json]
|
|
91
|
+
elsif mode == 'save' # Saving
|
|
92
|
+
File.open(expanded_path, 'w') { |f| f.print request.params['data'] }
|
|
93
|
+
File.open(message_file, 'w') { |f| f.print request.params['message'] } unless request.params['message'] == ''
|
|
94
|
+
redirect! "/" + idx.to_s + file_path
|
|
95
|
+
elsif mode == 'upload' # Uploading
|
|
96
|
+
halt 404 unless file = request.params['file']
|
|
97
|
+
tempfile, filename = file[:tempfile], file[:filename]
|
|
98
|
+
FileUtils.mv(tempfile.path, File.expand_path(filename, expanded_path))
|
|
99
|
+
redirect! "/" + idx.to_s + file_path + "/" + filename
|
|
100
|
+
elsif !File.exist?(expanded_path) && !request.params['dir'] # edit for non-existent file
|
|
101
|
+
FileUtils.mkdir_p(File.dirname(expanded_path))
|
|
102
|
+
FileUtils.touch(expanded_path)
|
|
103
|
+
redirect! "/" + idx.to_s + file_path + "?mode=edit"
|
|
104
|
+
elsif !File.exist?(expanded_path) && request.params['dir'] # create directory
|
|
105
|
+
FileUtils.mkdir_p(expanded_path)
|
|
106
|
+
redirect! "/" + idx.to_s + file_path
|
|
107
|
+
elsif File.directory?(expanded_path) # list directory
|
|
108
|
+
contents = gd.dir_files(expanded_path)
|
|
109
|
+
rendered_readme = nil
|
|
110
|
+
if readme = Dir[File.expand_path("README.{md,txt}", expanded_path)].first
|
|
111
|
+
rendered_readme = '<h3>' + File.basename(readme) + '</h3><div class="tilt">' + render(readme) + '</div>'
|
|
112
|
+
end
|
|
113
|
+
render! "dir", :layout => 'app', :locals => locals.merge(:contents => contents, :rendered_readme => rendered_readme)
|
|
114
|
+
elsif mode == "revisions" # list revisions
|
|
115
|
+
revisions = gd.file_revisions(file_path)
|
|
116
|
+
render! "revisions", :layout => 'app', :locals => locals.merge(:revisions => revisions)
|
|
117
|
+
elsif mode == "revert" # revert file
|
|
118
|
+
if revision = request.params['revision']
|
|
119
|
+
File.open(message_file, 'w') { |f| f.print "Reverting '#{file_path}' to #{revision}" }
|
|
120
|
+
gd.file_revert(file_path, revision)
|
|
121
|
+
end
|
|
122
|
+
redirect! "/" + idx.to_s + file_path
|
|
123
|
+
elsif mode == 'delete' # delete file
|
|
124
|
+
FileUtils.rm(expanded_path)
|
|
125
|
+
redirect! "/" + idx.to_s + parent
|
|
126
|
+
elsif mode == 'edit' && (mime.match(%r{text/}) || mime.match(%r{x-empty})) # edit file
|
|
127
|
+
contents = File.read(expanded_path)
|
|
128
|
+
render! "edit", :layout => 'app', :locals => locals.merge(:contents => contents)
|
|
129
|
+
elsif mode != 'raw' # render file
|
|
130
|
+
revision = request.params['revision']
|
|
131
|
+
expanded_path = gd.file_revision_at(file_path, revision) if revision
|
|
132
|
+
begin # attempting to render file
|
|
133
|
+
contents = '<div class="tilt">' + render(expanded_path) + '</div>'
|
|
134
|
+
rescue RuntimeError => e # not tilt supported
|
|
135
|
+
contents = if mime.match(%r{text/})
|
|
136
|
+
'<pre class="CodeRay">' + CodeRay.scan_file(expanded_path).encode(:html) + '</pre>'
|
|
137
|
+
else
|
|
138
|
+
%|<embed class="inline-file" src="/#{idx}#{request.path_info}?mode=raw"></embed>|
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
render! "file", :layout => 'app', :locals => locals.merge(:contents => contents,
|
|
142
|
+
:pygments_css => Pygments.css('.highlight'))
|
|
143
|
+
else # other file
|
|
144
|
+
run! Rack::File.new(gd.root)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
}.setup {
|
|
149
|
+
views_path File.expand_path("../views", __FILE__)
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<script src="/js/ace/ace.js" type="text/javascript" charset="utf-8"></script>
|
|
2
|
+
<script src="/js/ace/theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
|
|
3
|
+
<script src="/js/ace/mode-css.js" type="text/javascript" charset="utf-8"></script>
|
|
4
|
+
<script src="/js/ace/mode-html.js" type="text/javascript" charset="utf-8"></script>
|
|
5
|
+
<script src="/js/ace/mode-markdown.js" type="text/javascript" charset="utf-8"></script>
|
|
6
|
+
<script src="/js/ace/mode-javascript.js" type="text/javascript" charset="utf-8"></script>
|
|
7
|
+
<script src="/js/ace/mode-ruby.js" type="text/javascript" charset="utf-8"></script>
|
|
8
|
+
<script src="/js/edit.js" type="text/javascript" charset="utf-8"></script>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
%h2.path
|
|
2
|
+
%span.path= request.path_info.empty? ? '/' : request.path_info
|
|
3
|
+
- if file
|
|
4
|
+
%ul{ :class => "tabs" }
|
|
5
|
+
%li
|
|
6
|
+
%a{ :href => "#{request.path}" } View
|
|
7
|
+
%li
|
|
8
|
+
%a{ :href => "?mode=raw" } Raw
|
|
9
|
+
%li
|
|
10
|
+
%a{ :href => "?mode=edit" } Edit
|
|
11
|
+
%li
|
|
12
|
+
%a{ :href => "?mode=revisions" } Revisions
|
|
13
|
+
%li
|
|
14
|
+
%a{ :href => "?mode=delete", :onclick => "javascript:return confirm('Are you sure?')" } Delete
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
!!!
|
|
2
|
+
%html
|
|
3
|
+
%head
|
|
4
|
+
%meta{ "http-equiv" => "content-type", :content => "text/html; charset=UTF-8" }
|
|
5
|
+
%title Gitdocs #{Gitdocs::VERSION}
|
|
6
|
+
%link{ :href => "/css/bootstrap.css", :rel => "stylesheet"}
|
|
7
|
+
%link{ :href => "/css/app.css", :rel => "stylesheet" }
|
|
8
|
+
%link{ :href => "/css/tilt.css", :rel => "stylesheet" }
|
|
9
|
+
%link{ :href => "/css/coderay.css", :rel => "stylesheet" }
|
|
10
|
+
%script{ :src => "/js/util.js", :type => "text/javascript", :charset => "utf-8" }
|
|
11
|
+
%script{ :src => "/js/jquery.js", :type => "text/javascript", :charset => "utf-8" }
|
|
12
|
+
%script{ :src => "/js/jquery.tablesorter.js", :type => "text/javascript", :charset => "utf-8" }
|
|
13
|
+
%script{ :src => "/js/bootstrap-alerts.js", :type => "text/javascript", :charset => "utf-8" }
|
|
14
|
+
%script{ :src => "/js/app.js", :type => "text/javascript", :charset => "utf-8" }
|
|
15
|
+
- if @css
|
|
16
|
+
%style{:type => "text/css"}
|
|
17
|
+
= @css
|
|
18
|
+
%body
|
|
19
|
+
#nav.topbar
|
|
20
|
+
.fill
|
|
21
|
+
.container
|
|
22
|
+
%a{:class => "brand", :href => "/"} Gitdocs
|
|
23
|
+
%ul(class="nav")
|
|
24
|
+
%li{ :class => ("active" if nav_state == "home") }
|
|
25
|
+
%a(href = "/") Home
|
|
26
|
+
%li{ :class => ("active" if nav_state == "settings") }
|
|
27
|
+
%a(href = "/settings") Settings
|
|
28
|
+
%form{:class => "pull-left", :action => "/search", :method => 'GET'}
|
|
29
|
+
%input{:type => "text", :placeholder => "Search", :name => 'q'}
|
|
30
|
+
|
|
31
|
+
#main.container
|
|
32
|
+
.content
|
|
33
|
+
- if @title
|
|
34
|
+
.page-header
|
|
35
|
+
%h1= @title
|
|
36
|
+
.row
|
|
37
|
+
.span16= preserve(yield)
|
|
38
|
+
|
|
39
|
+
%footer
|
|
40
|
+
%p © Gitdocs v#{Gitdocs::VERSION}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
- @title = root
|
|
2
|
+
|
|
3
|
+
= partial("header", :locals => { :parent => parent, :file => false, :idx => idx })
|
|
4
|
+
|
|
5
|
+
- if contents && contents.any?
|
|
6
|
+
%table#fileListing.condensed-table.zebra-striped
|
|
7
|
+
%thead
|
|
8
|
+
%tr
|
|
9
|
+
%th File
|
|
10
|
+
%th Author
|
|
11
|
+
%th Last Modified
|
|
12
|
+
%th Size
|
|
13
|
+
|
|
14
|
+
%tbody
|
|
15
|
+
- contents.each_with_index do |f, i|
|
|
16
|
+
%tr
|
|
17
|
+
%td
|
|
18
|
+
%img{ :src => "/img/#{f.dir? ? 'folder' : 'file'}.png", :width => 16, :height => 16 }
|
|
19
|
+
%a{ :href => "/#{idx}#{request.path_info}/#{f.name}" }
|
|
20
|
+
= f.name
|
|
21
|
+
%td.author
|
|
22
|
+
%td.modified
|
|
23
|
+
%td.size
|
|
24
|
+
|
|
25
|
+
- if contents.empty?
|
|
26
|
+
%p No files were found in this directory.
|
|
27
|
+
|
|
28
|
+
.row
|
|
29
|
+
.span6
|
|
30
|
+
%form.upload{ :method => "post", :enctype => "multipart/form-data", :action => "/#{idx}#{request.path_info}?mode=upload" }
|
|
31
|
+
%p Upload file to this directory
|
|
32
|
+
%input{:type => 'file', :value => "Select a file", :name => "file", :class => "uploader", :size => 12}
|
|
33
|
+
%input{:type => 'submit', :value => "Upload file", :class => "btn secondary" }
|
|
34
|
+
.span8
|
|
35
|
+
%form.add
|
|
36
|
+
%p Add new file or directory
|
|
37
|
+
%input{:type => 'text', :name => "path", :class => "edit", :placeholder => "somefile.md or somedir" }
|
|
38
|
+
%input{:type => 'submit', :value => "New file", :class => "btn secondary file" }
|
|
39
|
+
%input{:type => 'submit', :value => "New directory", :class => "btn secondary directory" }
|
|
40
|
+
|
|
41
|
+
- if rendered_readme
|
|
42
|
+
.contents
|
|
43
|
+
= preserve rendered_readme
|