├── .gitignore ├── images └── search.png ├── public ├── favicon.ico ├── app.js ├── sh_diff.min.js ├── sh_style.css ├── style.css ├── jeditable.min.js ├── sh_main.min.js └── jquery.min.js ├── .gitmodules ├── AUTHORS ├── README.textile ├── environment.rb ├── views ├── attach.erb ├── delta.erb ├── history.erb ├── search.erb ├── list.erb ├── branch_history.erb ├── layout.erb ├── show.erb ├── edit.erb └── branches.erb ├── extensions.rb ├── system └── ruby.server.gitwiki.plist ├── TODO ├── LICENSE ├── page.rb └── git-wiki.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store -------------------------------------------------------------------------------- /images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al3x/git-wiki/master/images/search.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al3x/git-wiki/master/public/favicon.ico -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "sinatra"] 2 | path = sinatra 3 | url = git://github.com/bmizerany/sinatra.git 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Originally by Simon Rozet (http://atonie.org/2008/02/git-wiki) 2 | 3 | Modified by: 4 | - Alex Payne (http://www.al3x.net) 5 | - Jesse Andrews (http://www.overstimulate.com) 6 | - Timoni Grone (http://www.timoni.org) - stylesheet and design aid 7 | - Scott Chacon (http://jointheconversation.org) - ruby-git migration -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h1. git-wiki 2 | 3 | A wiki engine that uses a Git repository as its data store. 4 | 5 | h2. Status 6 | 7 | Alex Payne (see AUTHORS file) is no longer actively developing this branch. 8 | Please fork from here and continue development! 9 | 10 | h2. Requirements 11 | 12 | * rubygems 13 | * sinatra 14 | * grit 15 | * redcloth 16 | * rubypants -------------------------------------------------------------------------------- /environment.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'extensions' 3 | require 'page' 4 | 5 | %w(git redcloth rubypants).each do |gem| 6 | require_gem_with_feedback gem 7 | end 8 | 9 | GIT_REPO = ENV['HOME'] + '/wiki' 10 | HOMEPAGE = 'home' 11 | 12 | unless File.exists?(GIT_REPO) && File.directory?(GIT_REPO) 13 | puts "Initializing repository in #{GIT_REPO}..." 14 | Git.init(GIT_REPO) 15 | end 16 | 17 | $repo = Git.open(GIT_REPO) -------------------------------------------------------------------------------- /views/attach.erb: -------------------------------------------------------------------------------- 1 |

Attach File

2 | 3 |
4 |

New File Upload

5 |
7 | 8 | 9 |
10 | 11 | 12 |
13 | 14 | 15 |
16 |
17 | 18 | -------------------------------------------------------------------------------- /extensions.rb: -------------------------------------------------------------------------------- 1 | def require_gem_with_feedback(gem) 2 | begin 3 | require gem 4 | rescue LoadError 5 | puts "You need to 'sudo gem install #{gem}' before we can proceed" 6 | end 7 | end 8 | 9 | class String 10 | def wiki_linked 11 | self.gsub!(/\b((?:[A-Z]\w+){2,})/) { |m| "#{m}" } 12 | self.gsub!(/\[(\w+){2,}\]/) { |m| 13 | m.gsub!(/(\[|\])/, '') 14 | "#{m}" 15 | } 16 | self 17 | end 18 | end 19 | 20 | class Time 21 | def for_time_ago_in_words 22 | "#{(self.to_i * 1000)}" 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /views/delta.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Diff of <%= @page.name %>

6 | 7 | 12 | 13 |
14 |
<%= @page.delta(params[:rev]) %>
15 |
16 | 17 | 20 | -------------------------------------------------------------------------------- /views/history.erb: -------------------------------------------------------------------------------- 1 |

History of <%= @page.name %>

2 | 3 | 7 | 8 |
9 | 23 |
24 | -------------------------------------------------------------------------------- /views/search.erb: -------------------------------------------------------------------------------- 1 | <% if @grep.empty? %> 2 |

No pages match

3 | <% else %> 4 |

Results for '<%= @search %>'

5 | <% end %> 6 | 7 | 13 | 14 |
15 | <% @grep.each do |sha, arr_match| %> 16 |
17 | <% (sha, file) = sha.split(':') %> 18 | <% arr_match.each do |line, match| %> 19 |
20 | <%= match %> 21 |
22 | — <%= file %>, line <%= line %> (<%= $repo.object(sha).name %>) 23 |
24 | <% end %> 25 |
26 | <% end %> 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /views/list.erb: -------------------------------------------------------------------------------- 1 | <% if @pages.empty? %> 2 |

No pages yet

3 | <% else %> 4 |

All pages

5 | 6 |
7 | 17 |
18 | <% end %> 19 | -------------------------------------------------------------------------------- /views/branch_history.erb: -------------------------------------------------------------------------------- 1 |

Branch History

2 | 3 | 7 | 8 |
9 |
10 | 29 |
30 | 31 | -------------------------------------------------------------------------------- /system/ruby.server.gitwiki.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | ruby.server.gitwiki 7 | 8 | WorkingDirectory 9 | 10 | /Users/al3x/src/git-wiki 11 | 12 | Program 13 | git-wiki.rb 14 | 15 | ProgramArguments 16 | 17 | -e production 18 | -p 8777 19 | 20 | 21 | KeepAlive 22 | 23 | 24 | RunAtLoad 25 | 26 | 27 | UserName 28 | 29 | al3x 30 | 31 | Debug 32 | 33 | 34 | EnvironmentVariables 35 | 36 | PATH 37 | /usr/local/bin:/opt/local/bin:/opt/local/sbin:/usr/local/mysql/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/usr/X11/bin 38 | 39 | 40 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | = SOON 2 | * repo stats/info/maintenance page 3 | * deleting pages 4 | * pushing repo from web interface 5 | 6 | = CHACON IDEAS 7 | * tagging 8 | * file attachments 9 | * inter-branch links 10 | * merge conflict resolution 11 | * git-less version (read-only) 12 | * track which branched from which 13 | * push? 14 | * cherry-picked branches (read-tree/write-tree) 15 | * users (email/name/ip - use for commits) 16 | 17 | = LATER/MAYBE 18 | * backlinks (trivial syntax, ideally) 19 | * outliner functionality 20 | * menubar item launcher (like Instiki back in the day) 21 | * RubyCocoa GUI 22 | 23 | = DONE 24 | * working launchd plist 25 | * in-place editing 26 | * fix duplication of routes/methods to accommodate file extensions 27 | * "append selected text/link to page" bookmarklet (shamelessly cribbed 28 | from Trsly/Instapaper) 29 | * allow "code pages" by supporting file extensions, applying syntax 30 | highlighting (removed March 14 2008 by al3x, may add back in) 31 | * next/previous commit links on pages 32 | * pretty stylesheet 33 | 34 | = PROBABLY NOT 35 | * Shoes GUI: Shoes is still pretty immature, ex: all paths are relative 36 | to the `shoes` binary making requires a pain, etc. -------------------------------------------------------------------------------- /public/app.js: -------------------------------------------------------------------------------- 1 | /* cribbed from http://nullstyle.com/2007/06/02/caching-time_ago_in_words */ 2 | function time_ago_in_words(from) { 3 | return distance_of_time_in_words(new Date(), new Date(from)) 4 | } 5 | 6 | function distance_of_time_in_words(to, from) { 7 | seconds_ago = ((to - from) / 1000); 8 | minutes_ago = Math.floor(seconds_ago / 60); 9 | 10 | if(minutes_ago <= 0) { return "less than a minute"; } 11 | if(minutes_ago == 1) { return "a minute"; } 12 | if(minutes_ago < 45) { return minutes_ago + " minutes"; } 13 | if(minutes_ago < 90) { return "1 hour"; } 14 | hours_ago = Math.round(minutes_ago / 60); 15 | if(minutes_ago < 1440) { return hours_ago + " hours"; } 16 | if(minutes_ago < 2880) { return "1 day"; } 17 | days_ago = Math.round(minutes_ago / 1440); 18 | if(minutes_ago < 43200) { return days_ago + " days"; } 19 | if(minutes_ago < 86400) { return "1 month"; } 20 | months_ago = Math.round(minutes_ago / 43200); 21 | if(minutes_ago < 525960) { return months_ago + " months"; } 22 | if(minutes_ago < 1051920) { return "1 year"; } 23 | years_ago = Math.round(minutes_ago / 525960); 24 | return "over " + years_ago + " years" 25 | } 26 | 27 | function clearField(e) { 28 | if (e.cleared) { return; } 29 | e.cleared = true; 30 | e.value = ''; 31 | e.style.color = '#333'; 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (C) 2004 Sam Hocevar 4 | 14 rue de Plaisance, 75014 Paris, France 5 | Everyone is permitted to copy and distribute verbatim or modified 6 | copies of this license document, and changing it is allowed as long 7 | as the name is changed. 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | ======= 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /views/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= @title %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 22 |
23 | <%= yield %> 24 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /public/sh_diff.min.js: -------------------------------------------------------------------------------- 1 | 2 | if(!this.sh_languages){this.sh_languages={};} 3 | sh_languages['diff']=[[{'next':1,'regex':/(?=^[-]{3})/g,'state':1,'style':'sh_oldfile'},{'next':6,'regex':/(?=^[*]{3})/g,'state':1,'style':'sh_oldfile'},{'next':14,'regex':/(?=^[\d])/g,'state':1,'style':'sh_difflines'}],[{'next':2,'regex':/^[-]{3}/g,'style':'sh_oldfile'},{'next':3,'regex':/^[-]/g,'style':'sh_oldfile'},{'next':4,'regex':/^[+]/g,'style':'sh_newfile'},{'next':5,'regex':/^@@/g,'style':'sh_difflines'}],[{'exit':true,'regex':/$/g}],[{'exit':true,'regex':/$/g}],[{'exit':true,'regex':/$/g}],[{'exit':true,'regex':/$/g}],[{'next':7,'regex':/^[*]{3}[ \t]+[\d]/g,'style':'sh_oldfile'},{'next':9,'regex':/^[*]{3}/g,'style':'sh_oldfile'},{'next':10,'regex':/^[-]{3}[ \t]+[\d]/g,'style':'sh_newfile'},{'next':13,'regex':/^[-]{3}/g,'style':'sh_newfile'}],[{'next':8,'regex':/^[\s]/g,'style':'sh_normal'},{'exit':true,'regex':/(?=^[-]{3})/g,'style':'sh_newfile'}],[{'exit':true,'regex':/$/g}],[{'exit':true,'regex':/$/g}],[{'next':11,'regex':/^[\s]/g,'style':'sh_normal'},{'exit':true,'regex':/(?=^[*]{3})/g,'style':'sh_newfile'},{'exit':true,'next':12,'regex':/^diff/g,'style':'sh_normal'}],[{'exit':true,'regex':/$/g}],[{'exit':true,'regex':/$/g}],[{'exit':true,'regex':/$/g}],[{'next':15,'regex':/^[\d]/g,'style':'sh_difflines'},{'next':16,'regex':/^[<]/g,'style':'sh_oldfile'},{'next':17,'regex':/^[>]/g,'style':'sh_newfile'}],[{'exit':true,'regex':/$/g}],[{'exit':true,'regex':/$/g}],[{'exit':true,'regex':/$/g}]]; -------------------------------------------------------------------------------- /views/show.erb: -------------------------------------------------------------------------------- 1 | 16 | 17 |

<%= @page.name %>

18 | 19 | <%= @env %> 20 | 21 | 47 | 48 |
<%= @page.body %>
-------------------------------------------------------------------------------- /views/edit.erb: -------------------------------------------------------------------------------- 1 |

Editing <%= @page.name %>

2 | 3 | 29 | 30 |
31 |
32 | 33 | Message: 34 | 35 |

36 |
37 |
-------------------------------------------------------------------------------- /views/branches.erb: -------------------------------------------------------------------------------- 1 |

Branches

2 | 3 | 17 | 18 |
19 |

New Branch

20 |
21 | 22 | 23 |
24 | 25 | 26 | 27 | 36 | 37 | 38 | 39 |
40 | 41 |

42 | 43 |

New Remote Branch

44 |
45 | 46 | 47 |
48 | 49 | 50 | 51 |
52 | 53 | 54 | 55 |
56 |
57 | 58 | -------------------------------------------------------------------------------- /public/sh_style.css: -------------------------------------------------------------------------------- 1 | pre.sh_sourceCode { 2 | background-color: #ffffff; 3 | color: #000000; 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | 8 | pre.sh_sourceCode .sh_keyword { 9 | color: #7f0055; 10 | font-weight: bold; 11 | font-style: normal; 12 | } 13 | 14 | pre.sh_sourceCode .sh_type { 15 | color: #7f0055; 16 | font-weight: bold; 17 | font-style: normal; 18 | } 19 | 20 | pre.sh_sourceCode .sh_string { 21 | color: #0000ff; 22 | font-weight: normal; 23 | font-style: normal; 24 | } 25 | 26 | pre.sh_sourceCode .sh_regexp { 27 | color: #0000ff; 28 | font-weight: normal; 29 | font-style: normal; 30 | } 31 | 32 | pre.sh_sourceCode .sh_specialchar { 33 | color: #0000ff; 34 | font-weight: normal; 35 | font-style: normal; 36 | } 37 | 38 | pre.sh_sourceCode .sh_comment { 39 | color: #717ab3; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | pre.sh_sourceCode .sh_number { 45 | color: #000000; 46 | font-weight: normal; 47 | font-style: normal; 48 | } 49 | 50 | pre.sh_sourceCode .sh_preproc { 51 | color: #3f5fbf; 52 | font-weight: normal; 53 | font-style: normal; 54 | } 55 | 56 | pre.sh_sourceCode .sh_function { 57 | color: #000000; 58 | font-weight: normal; 59 | font-style: normal; 60 | } 61 | 62 | pre.sh_sourceCode .sh_url { 63 | color: #0000ff; 64 | font-weight: normal; 65 | font-style: normal; 66 | } 67 | 68 | pre.sh_sourceCode .sh_date { 69 | color: #7f0055; 70 | font-weight: bold; 71 | font-style: normal; 72 | } 73 | 74 | pre.sh_sourceCode .sh_time { 75 | color: #7f0055; 76 | font-weight: bold; 77 | font-style: normal; 78 | } 79 | 80 | pre.sh_sourceCode .sh_file { 81 | color: #7f0055; 82 | font-weight: bold; 83 | font-style: normal; 84 | } 85 | 86 | pre.sh_sourceCode .sh_ip { 87 | color: #0000ff; 88 | font-weight: normal; 89 | font-style: normal; 90 | } 91 | 92 | pre.sh_sourceCode .sh_name { 93 | color: #0000ff; 94 | font-weight: normal; 95 | font-style: normal; 96 | } 97 | 98 | pre.sh_sourceCode .sh_variable { 99 | color: #7f0055; 100 | font-weight: bold; 101 | font-style: normal; 102 | } 103 | 104 | pre.sh_sourceCode .sh_oldfile { 105 | color: #0000ff; 106 | font-weight: normal; 107 | font-style: normal; 108 | } 109 | 110 | pre.sh_sourceCode .sh_newfile { 111 | color: #0000ff; 112 | font-weight: normal; 113 | font-style: normal; 114 | } 115 | 116 | pre.sh_sourceCode .sh_difflines { 117 | color: #7f0055; 118 | font-weight: bold; 119 | font-style: normal; 120 | } 121 | 122 | pre.sh_sourceCode .sh_selector { 123 | color: #7f0055; 124 | font-weight: bold; 125 | font-style: normal; 126 | } 127 | 128 | pre.sh_sourceCode .sh_property { 129 | color: #7f0055; 130 | font-weight: bold; 131 | font-style: normal; 132 | } 133 | 134 | pre.sh_sourceCode .sh_value { 135 | color: #0000ff; 136 | font-weight: normal; 137 | font-style: normal; 138 | } 139 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | /* elements */ 2 | 3 | body { 4 | background-color: #fff; 5 | color: #333; 6 | font-family: Helvetica, Arial, "Deja Vu Sans", "Bitstream Vera Sans", sans-serif; 7 | font-size: .95em; 8 | line-height: 1.6em; 9 | margin: 2em; 10 | } 11 | 12 | a { 13 | color: #6d7fa3; 14 | text-decoration: none; 15 | } 16 | a:visited { color: #7b69b0; } 17 | a:hover { text-decoration: underline; } 18 | 19 | code, pre { 20 | font-family: "Deja Vu Sans Mono", "Bitstream Vera Sans Mono", "Inconsolata", "Consolas", monospace; 21 | } 22 | 23 | h1 { 24 | color: #333; 25 | font-size: 3.5em; 26 | margin: 24px 0 8px 0; 27 | } 28 | 29 | input[type=text] { 30 | border: 1px solid #ccc; 31 | font-family: Helvetica, Arial, "Deja Vu Sans", "Bitstream Vera Sans", sans-serif; 32 | font-size: 0.875em; 33 | } 34 | 35 | label { font-size: .9em; } 36 | 37 | table { 38 | font-size: 0.9em; 39 | width: 100%; 40 | } 41 | 42 | textarea { 43 | border: 1px solid #ccc; 44 | font-family: "Deja Vu Sans Mono", "Bitstream Vera Sans Mono", "Inconsolata", "Consolas", monospace; 45 | font-size: .9em; 46 | padding: 5px; 47 | } 48 | 49 | ul { 50 | list-style-type: square; 51 | padding: 0 0 1em 1.4em; 52 | } 53 | 54 | /* classes */ 55 | 56 | .cancel { 57 | font-weight: normal; 58 | text-decoration: none; 59 | font-size: 14px; 60 | } 61 | .cancel:before { content: "("; } 62 | .cancel:after { content: ")" } 63 | 64 | .clearer { clear: both; } 65 | 66 | .content { 67 | margin-left: 35%; 68 | padding: 10px 10px 2px 10px; 69 | } 70 | 71 | .content h1, .content h2, .content h3, .content h4 { 72 | padding: 0 0 .4em 0; 73 | margin: 0 0 0 0; 74 | } 75 | .content h1 { font-size: 2.6em; } 76 | .content h2 { font-size: 2em; } 77 | .content h3 { font-size: 1.8em; } 78 | .content h4 { font-size: 1.6em; } 79 | 80 | .delta { 81 | font-size: 1.1em; 82 | padding: 0; 83 | } 84 | 85 | .edit textarea { 86 | display: block; 87 | margin-bottom: 5px; 88 | max-height: 300px; 89 | min-width: 100%; 90 | } 91 | 92 | .nav_link, .nav_link:visited { 93 | display: inline; 94 | padding: 3px; 95 | color: #666; 96 | } 97 | 98 | .nav_link:hover { 99 | background-color: #d1ccdb; 100 | color: #333; 101 | text-decoration: none; 102 | } 103 | 104 | .nav_list, #footer { 105 | border-top: 1px solid #ccc; 106 | border-bottom: 1px solid #ccc; 107 | list-style-type: none; 108 | padding: 4px; 109 | text-align: right; 110 | } 111 | 112 | .nav_list li { 113 | display: inline; 114 | padding-right: 10px; 115 | } 116 | 117 | .niceform label, .niceform input[type=text] { 118 | display: block; 119 | width: 200px; 120 | float: left; 121 | margin-bottom: 10px; 122 | } 123 | 124 | .niceform label { 125 | text-align: right; 126 | width: 150px; 127 | padding-right: 20px; 128 | } 129 | 130 | .niceform br { 131 | clear: left; 132 | } 133 | 134 | .right { text-align: right; } 135 | 136 | .search_result { margin-bottom: 15px; } 137 | 138 | .search_result .match { 139 | line-height: 1em; 140 | margin-bottom: 15px; 141 | } 142 | 143 | .search_term { color: #999; } 144 | 145 | .submit { 146 | font-size: 1.2em; 147 | font-weight: bold; 148 | } 149 | 150 | .sub_nav { 151 | float: left; 152 | padding-top: 8px; 153 | } 154 | 155 | .sub_nav .details { 156 | color: #888; 157 | font-size: .85em; 158 | margin-left: 0.3em; 159 | } 160 | span.detail { 161 | color: #888; 162 | font-size: .85em; 163 | } 164 | div.attach-options { 165 | margin-left: 30px; 166 | margin-bottom: 10px; 167 | font-size: .85em; 168 | color: #888; 169 | } 170 | /* ids */ 171 | 172 | #container { 173 | clear: both; 174 | margin: auto; 175 | width: 70%; 176 | } 177 | 178 | #edit_textarea { 179 | height: 35em; 180 | margin-top: 1.5em; 181 | width: 100%; 182 | } 183 | #message_textarea { 184 | height: 4em; 185 | width: 100%; 186 | } 187 | 188 | #footer { clear: both; } 189 | #footer a, #footer a:visited { color: #666; } 190 | 191 | #search_field { 192 | border: 1px solid #ccc; 193 | color: #999; 194 | } 195 | -------------------------------------------------------------------------------- /page.rb: -------------------------------------------------------------------------------- 1 | class Page 2 | attr_reader :name, :attach_dir 3 | 4 | def initialize(name, rev=nil) 5 | @name = name 6 | @rev = rev 7 | @filename = File.join(GIT_REPO, @name) 8 | @attach_dir = File.join(GIT_REPO, '_attachments', unwiki(@name)) 9 | end 10 | 11 | def unwiki(string) 12 | string.downcase 13 | end 14 | 15 | def body 16 | @body ||= RubyPants.new(RedCloth.new(raw_body).to_html).to_html.wiki_linked 17 | end 18 | 19 | def branch_name 20 | $repo.current_branch 21 | end 22 | 23 | def updated_at 24 | commit.committer_date rescue Time.now 25 | end 26 | 27 | def raw_body 28 | if @rev 29 | @raw_body ||= blob.contents 30 | else 31 | @raw_body ||= File.exists?(@filename) ? File.read(@filename) : '' 32 | end 33 | end 34 | 35 | def update(content, message=nil) 36 | File.open(@filename, 'w') { |f| f << content } 37 | commit_message = tracked? ? "edited #{@name}" : "created #{@name}" 38 | commit_message += ' : ' + message if message && message.length > 0 39 | begin 40 | $repo.add(@name) 41 | $repo.commit(commit_message) 42 | rescue 43 | nil 44 | end 45 | @body = nil; @raw_body = nil 46 | @body 47 | end 48 | 49 | def tracked? 50 | $repo.ls_files.keys.include?(@name) 51 | end 52 | 53 | def history 54 | return nil unless tracked? 55 | @history ||= $repo.log.path(@name) 56 | end 57 | 58 | def delta(rev) 59 | $repo.diff(previous_commit, rev).path(@name).patch 60 | end 61 | 62 | def commit 63 | @commit ||= $repo.log.object(@rev || 'master').path(@name).first 64 | end 65 | 66 | def previous_commit 67 | @previous_commit ||= $repo.log(2).object(@rev || 'master').path(@name).to_a[1] 68 | end 69 | 70 | def next_commit 71 | begin 72 | if (self.history.first.sha == self.commit.sha) 73 | @next_commit ||= nil 74 | else 75 | matching_index = nil 76 | history.each_with_index { |c, i| matching_index = i if c.sha == self.commit.sha } 77 | @next_commit ||= history.to_a[matching_index - 1] 78 | end 79 | rescue 80 | @next_commit ||= nil 81 | end 82 | end 83 | 84 | def version(rev) 85 | data = blob.contents 86 | RubyPants.new(RedCloth.new(data).to_html).to_html.wiki_linked 87 | end 88 | 89 | def blob 90 | @blob ||= ($repo.gblob(@rev + ':' + @name)) 91 | end 92 | 93 | # save a file into the _attachments directory 94 | def save_file(file, name = '') 95 | if name.size > 0 96 | filename = name + File.extname(file[:filename]) 97 | else 98 | filename = file[:filename] 99 | end 100 | FileUtils.mkdir_p(@attach_dir) if !File.exists?(@attach_dir) 101 | new_file = File.join(@attach_dir, filename) 102 | 103 | f = File.new(new_file, 'w') 104 | f.write(file[:tempfile].read) 105 | f.close 106 | 107 | commit_message = "uploaded #{filename} for #{@name}" 108 | begin 109 | $repo.add(new_file) 110 | $repo.commit(commit_message) 111 | rescue 112 | nil 113 | end 114 | end 115 | 116 | def delete_file(file) 117 | file_path = File.join(@attach_dir, file) 118 | if File.exists?(file_path) 119 | File.unlink(file_path) 120 | 121 | commit_message = "removed #{file} for #{@name}" 122 | begin 123 | $repo.remove(file_path) 124 | $repo.commit(commit_message) 125 | rescue 126 | nil 127 | end 128 | 129 | end 130 | end 131 | 132 | def attachments 133 | if File.exists?(@attach_dir) 134 | return Dir.glob(File.join(@attach_dir, '*')).map { |f| Attachment.new(f, unwiki(@name)) } 135 | else 136 | false 137 | end 138 | end 139 | 140 | class Attachment 141 | attr_accessor :path, :page_name 142 | def initialize(file_path, name) 143 | @path = file_path 144 | @page_name = name 145 | end 146 | 147 | def name 148 | File.basename(@path) 149 | end 150 | 151 | def link_path 152 | File.join('/_attachment', @page_name, name) 153 | end 154 | 155 | def delete_path 156 | File.join('/a/file/delete', @page_name, name) 157 | end 158 | 159 | def image? 160 | ext = File.extname(@path) 161 | case ext 162 | when '.png', '.jpg', '.jpeg', '.gif'; return true 163 | else; return false 164 | end 165 | end 166 | 167 | def size 168 | size = File.size(@path).to_i 169 | case 170 | when size.to_i == 1; "1 Byte" 171 | when size < 1024; "%d Bytes" % size 172 | when size < (1024*1024); "%.2f KB" % (size / 1024.0) 173 | else "%.2f MB" % (size / (1024 * 1024.0)) 174 | end.sub(/([0-9])\.?0+ /, '\1 ' ) 175 | end 176 | end 177 | 178 | end -------------------------------------------------------------------------------- /git-wiki.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'fileutils' 4 | require 'environment' 5 | require 'sinatra/lib/sinatra' 6 | 7 | get('/') { redirect "/#{HOMEPAGE}" } 8 | 9 | # page paths 10 | 11 | get '/:page' do 12 | @page = Page.new(params[:page]) 13 | @page.tracked? ? show(:show, @page.name) : redirect('/e/' + @page.name) 14 | end 15 | 16 | get '/:page/raw' do 17 | @page = Page.new(params[:page]) 18 | @page.raw_body 19 | end 20 | 21 | get '/:page/append' do 22 | @page = Page.new(params[:page]) 23 | @page.body = @page.raw_body + "\n\n" + params[:text] 24 | redirect '/' + @page.name 25 | end 26 | 27 | get '/e/:page' do 28 | @page = Page.new(params[:page]) 29 | show :edit, "Editing #{@page.name}" 30 | end 31 | 32 | post '/e/:page' do 33 | @page = Page.new(params[:page]) 34 | @page.update(params[:body], params[:message]) 35 | redirect '/' + @page.name 36 | end 37 | 38 | post '/eip/:page' do 39 | @page = Page.new(params[:page]) 40 | @page.update(params[:body]) 41 | @page.body 42 | end 43 | 44 | get '/h/:page' do 45 | @page = Page.new(params[:page]) 46 | show :history, "History of #{@page.name}" 47 | end 48 | 49 | get '/h/:page/:rev' do 50 | @page = Page.new(params[:page], params[:rev]) 51 | show :show, "#{@page.name} (version #{params[:rev]})" 52 | end 53 | 54 | get '/d/:page/:rev' do 55 | @page = Page.new(params[:page]) 56 | show :delta, "Diff of #{@page.name}" 57 | end 58 | 59 | # application paths (/a/ namespace) 60 | 61 | get '/a/list' do 62 | pages = $repo.log.first.gtree.children 63 | @pages = pages.select { |f,bl| f[0,1] != '_'}.sort.map { |name, blob| Page.new(name) } rescue [] 64 | show(:list, 'Listing pages') 65 | end 66 | 67 | get '/a/patch/:page/:rev' do 68 | @page = Page.new(params[:page]) 69 | header 'Content-Type' => 'text/x-diff' 70 | header 'Content-Disposition' => 'filename=patch.diff' 71 | @page.delta(params[:rev]) 72 | end 73 | 74 | get '/a/tarball' do 75 | header 'Content-Type' => 'application/x-gzip' 76 | header 'Content-Disposition' => 'filename=archive.tgz' 77 | archive = $repo.archive('HEAD', nil, :format => 'tgz', :prefix => 'wiki/') 78 | File.open(archive).read 79 | end 80 | 81 | get '/a/branches' do 82 | @branches = $repo.branches 83 | show :branches, "Branches List" 84 | end 85 | 86 | get '/a/branch/:branch' do 87 | $repo.checkout(params[:branch]) 88 | redirect '/' + HOMEPAGE 89 | end 90 | 91 | get '/a/history' do 92 | @history = $repo.log 93 | show :branch_history, "Branch History" 94 | end 95 | 96 | get '/a/revert_branch/:sha' do 97 | $repo.with_temp_index do 98 | $repo.read_tree params[:sha] 99 | $repo.checkout_index 100 | $repo.commit('reverted branch') 101 | end 102 | redirect '/a/history' 103 | end 104 | 105 | get '/a/merge_branch/:branch' do 106 | $repo.merge(params[:branch]) 107 | redirect '/' + HOMEPAGE 108 | end 109 | 110 | get '/a/delete_branch/:branch' do 111 | $repo.branch(params[:branch]).delete 112 | redirect '/a/branches' 113 | end 114 | 115 | post '/a/new_branch' do 116 | $repo.branch(params[:branch]).create 117 | $repo.checkout(params[:branch]) 118 | if params[:type] == 'blank' 119 | # clear out the branch 120 | $repo.chdir do 121 | Dir.glob("*").each do |f| 122 | File.unlink(f) 123 | $repo.remove(f) 124 | end 125 | touchfile 126 | $repo.commit('clean branch start') 127 | end 128 | end 129 | redirect '/a/branches' 130 | end 131 | 132 | post '/a/new_remote' do 133 | $repo.add_remote(params[:branch_name], params[:branch_url]) 134 | $repo.fetch(params[:branch_name]) 135 | redirect '/a/branches' 136 | end 137 | 138 | get '/a/search' do 139 | @search = params[:search] 140 | @grep = $repo.grep(@search) 141 | show :search, 'Search Results' 142 | end 143 | 144 | # file upload attachments 145 | 146 | get '/a/file/upload/:page' do 147 | @page = Page.new(params[:page]) 148 | show :attach, 'Attach File for ' + @page.name 149 | end 150 | 151 | post '/a/file/upload/:page' do 152 | @page = Page.new(params[:page]) 153 | @page.save_file(params[:file], params[:name]) 154 | redirect '/e/' + @page.name 155 | end 156 | 157 | get '/a/file/delete/:page/:file.:ext' do 158 | @page = Page.new(params[:page]) 159 | @page.delete_file(params[:file] + '.' + params[:ext]) 160 | redirect '/e/' + @page.name 161 | end 162 | 163 | get '/_attachment/:page/:file.:ext' do 164 | @page = Page.new(params[:page]) 165 | send_file(File.join(@page.attach_dir, params[:file] + '.' + params[:ext])) 166 | end 167 | 168 | # support methods 169 | 170 | def page_url(page) 171 | "#{request.env["rack.url_scheme"]}://#{request.env["HTTP_HOST"]}/#{page}" 172 | end 173 | 174 | private 175 | 176 | def show(template, title) 177 | @title = title 178 | erb(template) 179 | end 180 | 181 | def touchfile 182 | # adds meta file to repo so we have somthing to commit initially 183 | $repo.chdir do 184 | f = File.new(".meta", "w+") 185 | f.puts($repo.current_branch) 186 | f.close 187 | $repo.add('.meta') 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /public/jeditable.min.js: -------------------------------------------------------------------------------- 1 | 2 | (function($){$.fn.editable=function(target,options){var settings={target:target,name:'value',id:'id',type:'text',width:'auto',height:'auto',event:'click',onblur:'cancel',loadtype:'GET',loadtext:'Loading...',placeholder:'Click to edit',loaddata:{},submitdata:{}};if(options){$.extend(settings,options);} 3 | var plugin=$.editable.types[settings.type].plugin||function(){};var submit=$.editable.types[settings.type].submit||function(){};var buttons=$.editable.types[settings.type].buttons||$.editable.types['defaults'].buttons;var content=$.editable.types[settings.type].content||$.editable.types['defaults'].content;var element=$.editable.types[settings.type].element||$.editable.types['defaults'].element;var callback=settings.callback||function(){};if(!$.isFunction($(this)[settings.event])){$.fn[settings.event]=function(fn){return fn?this.bind(settings.event,fn):this.trigger(settings.event);}} 4 | $(this).attr('title',settings.tooltip);settings.autowidth='auto'==settings.width;settings.autoheight='auto'==settings.height;return this.each(function(){if(!$.trim($(this).html())){$(this).html(settings.placeholder);} 5 | $(this)[settings.event](function(e){var self=this;if(self.editing){return;} 6 | $(self).css("visibility","hidden");if(settings.width!='none'){settings.width=settings.autowidth?$(self).width():settings.width;} 7 | if(settings.height!='none'){settings.height=settings.autoheight?$(self).height():settings.height;} 8 | $(this).css("visibility","");if($(this).html().toLowerCase().replace(/;/,'')==settings.placeholder.toLowerCase().replace(/;/,'')){$(this).html('');} 9 | self.editing=true;self.revert=$(self).html();$(self).html('');var form=$('
');if(settings.cssclass){if('inherit'==settings.cssclass){form.attr('class',$(self).attr('class'));}else{form.attr('class',settings.cssclass);}} 10 | if(settings.style){if('inherit'==settings.style){form.attr('style',$(self).attr('style'));form.css('display',$(self).css('display'));}else{form.attr('style',settings.style);}} 11 | var input=element.apply(form,[settings,self]);var input_content;if(settings.loadurl){var t=setTimeout(function(){input.disabled=true;content.apply(form,[settings.loadtext,settings,self]);},100);var loaddata={};loaddata[settings.id]=self.id;if($.isFunction(settings.loaddata)){$.extend(loaddata,settings.loaddata.apply(self,[self.revert,settings]));}else{$.extend(loaddata,settings.loaddata);} 12 | $.ajax({type:settings.loadtype,url:settings.loadurl,data:loaddata,async:false,success:function(result){window.clearTimeout(t);input_content=result;input.disabled=false;}});}else if(settings.data){input_content=settings.data;if($.isFunction(settings.data)){input_content=settings.data.apply(self,[self.revert,settings]);}}else{input_content=self.revert;} 13 | content.apply(form,[input_content,settings,self]);input.attr('name',settings.name);buttons.apply(form,[settings,self]);plugin.apply(form,[settings,self]);$(self).append(form);$(':input:visible:enabled:first',form).focus();if(settings.select){input.select();} 14 | input.keydown(function(e){if(e.keyCode==27){e.preventDefault();reset();}});var t;if('cancel'==settings.onblur){input.blur(function(e){t=setTimeout(reset,500);});}else if('submit'==settings.onblur){input.blur(function(e){form.submit();});}else if($.isFunction(settings.onblur)){input.blur(function(e){settings.onblur.apply(self,[input.val(),settings]);});}else{input.blur(function(e){});} 15 | form.submit(function(e){if(t){clearTimeout(t);} 16 | e.preventDefault();submit.apply(form,[settings,self]);if($.isFunction(settings.target)){var str=settings.target.apply(self,[input.val(),settings]);$(self).html(str);self.editing=false;callback.apply(self,[self.innerHTML,settings]);if(!$.trim($(self).html())){$(self).html(settings.placeholder);}}else{var submitdata={};submitdata[settings.name]=input.val();submitdata[settings.id]=self.id;if($.isFunction(settings.submitdata)){$.extend(submitdata,settings.submitdata.apply(self,[self.revert,settings]));}else{$.extend(submitdata,settings.submitdata);} 17 | $(self).html(settings.indicator);$.post(settings.target,submitdata,function(str){$(self).html(str);self.editing=false;callback.apply(self,[self.innerHTML,settings]);if(!$.trim($(self).html())){$(self).html(settings.placeholder);}});} 18 | return false;});function reset(){$(self).html(self.revert);self.editing=false;if(!$.trim($(self).html())){$(self).html(settings.placeholder);}}});});};$.editable={types:{defaults:{element:function(settings,original){var input=$('');$(this).append(input);return(input);},content:function(string,settings,original){$(':input:first',this).val(string);},buttons:function(settings,original){if(settings.submit){var submit=$('');submit.val(settings.submit);$(this).append(submit);} 19 | if(settings.cancel){var cancel=$('');cancel.val(settings.cancel);$(this).append(cancel);$(cancel).click(function(){$(original).html(original.revert);original.editing=false;});}}},text:{element:function(settings,original){var input=$('');if(settings.width!='none'){input.width(settings.width);} 20 | if(settings.height!='none'){input.height(settings.height);} 21 | input.attr('autocomplete','off');$(this).append(input);return(input);}},textarea:{element:function(settings,original){var textarea=$('