require 'rubygems' require 'camping' require 'lib/git' # # gitweb is a web frontend on git # there is no user auth, so don't run this anywhere that anyone can use it # it's read only, but anyone can remove or add references to your repos # # install dependencies # sudo gem install camping-omnibus --source http://code.whytheluckystiff.net # # todo # - diff/patch between any two objects # - expand patch to entire file # - set title properly # - grep / search function # - prettify : http://projects.wh.techno-weenie.net/changesets/3030 # - add user model (add/remove repos) # - implement http-push for authenticated users # # author : scott chacon # Camping.goes :GitWeb module GitWeb::Models class Repository < Base; end class CreateGitWeb < V 0.1 def self.up create_table :gitweb_repositories, :force => true do |t| t.column :name, :string t.column :path, :string t.column :bare, :boolean end end end end module GitWeb::Controllers class Stylesheet < R '/css/highlight.css' def get @headers['Content-Type'] = 'text/css' ending = File.read(__FILE__).gsub(/.*__END__/m, '') ending.gsub(/__JS__.*/m, '') end end class JsHighlight < R '/js/highlight.js' def get @headers['Content-Type'] = 'text/css' File.read(__FILE__).gsub(/.*__JS__/m, '') end end class Index < R '/' def get @repos = Repository.find :all render :index end end class Add < R '/add' def get @repo = Repository.new render :add end def post if Git.bare(input.repository_path) repo = Repository.create :name => input.repo_name, :path => input.repo_path, :bare => input.repo_bare redirect View, repo else redirect Index end end end class RemoveRepo < R '/remove/(\d+)' def get repo_id @repo = Repository.find repo_id @repo.destroy @repos = Repository.find :all render :index end end class View < R '/view/(\d+)' def get repo_id @repo = Repository.find repo_id @git = Git.bare(@repo.path) render :view end end class Fetch < R '/git/(\d+)/(.*)' def get repo_id, path @repo = Repository.find repo_id @git = Git.bare(@repo.path) File.read(File.join(@git.repo.path, path)) end end class Commit < R '/commit/(\d+)/(\w+)' def get repo_id, sha @repo = Repository.find repo_id @git = Git.bare(@repo.path) @commit = @git.gcommit(sha) render :commit end end class Tree < R '/tree/(\d+)/(\w+)' def get repo_id, sha @repo = Repository.find repo_id @git = Git.bare(@repo.path) @tree = @git.gtree(sha) render :tree end end class Blob < R '/blob/(\d+)/(.*?)/(\w+)' def get repo_id, file, sha @repo = Repository.find repo_id logger = Logger.new('/tmp/git.log') logger.level = Logger::INFO @git = Git.bare(@repo.path, :log => logger) @blob = @git.gblob(sha) @file = file render :blob end end class BlobRaw < R '/blob/(\d+)/(\w+)' def get repo_id, sha @repo = Repository.find repo_id @git = Git.bare(@repo.path) @blob = @git.gblob(sha) @blob.contents end end class Archive < R '/archive/(\d+)/(\w+)' def get repo_id, sha @repo = Repository.find repo_id @git = Git.bare(@repo.path) file = @git.gtree(sha).archive @headers['Content-Type'] = 'application/zip' @headers["Content-Disposition"] = "attachment; filename=archive.zip" File.new(file).read end end class Download < R '/download/(\d+)/(.*?)/(\w+)' def get repo_id, file, sha @repo = Repository.find repo_id @git = Git.bare(@repo.path) @headers["Content-Disposition"] = "attachment; filename=#{file}" @git.gblob(sha).contents end end class Diff < R '/diff/(\d+)/(\w+)/(\w+)' def get repo_id, tree1, tree2 @repo = Repository.find repo_id @git = Git.bare(@repo.path) @tree1 = tree1 @tree2 = tree2 @diff = @git.diff(tree2, tree1) render :diff end end class Patch < R '/patch/(\d+)/(\w+)/(\w+)' def get repo_id, tree1, tree2 @repo = Repository.find repo_id @git = Git.bare(@repo.path) @diff = @git.diff(tree1, tree2).patch end end end module GitWeb::Views def layout html do head do title 'test' link :href=>R(Stylesheet), :rel=>'stylesheet', :type=>'text/css' script '', :type => "text/javascript", :language => "JavaScript", :src => R(JsHighlight) end style <<-END, :type => 'text/css' body { font-family: verdana, arial, helvetica, sans-serif; color: #333; font-size: 13px; line-height: 18px;} h1 { background: #cce; padding: 10px; margin: 3px; } h3 { background: #aea; padding: 5px; margin: 3px; } .options { float: right; margin: 10px; } p { padding: 5px; } .odd { background: #eee; } .tag { margin: 5px; padding: 1px 3px; border: 1px solid #8a8; background: #afa;} .indent { padding: 0px 15px;} table tr td { font-size: 13px; } table.shortlog { width: 100%; } .timer { color: #666; padding: 10px; margin-top: 10px; } END body :onload => "sh_highlightDocument();" do before = Time.now().usec self << yield self << '
' + ((Time.now().usec - before).to_f / 60).to_s + ' sec' end end end # git repo views def view h1 @repo.name h2 @repo.path gtags = @git.tags @tags = {} gtags.each { |tag| @tags[tag.sha] ||= []; @tags[tag.sha] << tag.name } url = 'http:' + URL(Fetch, @repo.id, '').to_s h3 'info' table.info do tr { td 'owner: '; td @git.config('user.name') } tr { td 'email: '; td @git.config('user.email') } tr { td 'url: '; td { a url, :href => url } } end h3 'shortlog' table.shortlog do @git.log.each do |log| tr do td log.date.strftime("%Y-%m-%d") td { code log.sha[0, 8] } td { em log.author.name } td do span.message log.message[0, 60] @tags[log.sha].each do |t| span.space ' ' span.tag { code t } end if @tags[log.sha] end td { a 'commit', :href => R(Commit, @repo, log.sha) } td { a 'commit-diff', :href => R(Diff, @repo, log.sha, log.parent.sha) } td { a 'tree', :href => R(Tree, @repo, log.gtree.sha) } td { a 'archive', :href => R(Archive, @repo, log.gtree.sha) } end end end h3 'branches' @git.branches.each do |branch| li { a branch.full, :href => R(Commit, @repo, branch.gcommit.sha) } end h3 'tags' gtags.each do |tag| li { a tag.name, :href => R(Commit, @repo, tag.sha) } end end def commit a.options 'repo', :href => R(View, @repo) h1 @commit.name h3 'info' table.info do tr { td 'author: '; td @commit.author.name + ' <' + @commit.author.email + '>'} tr { td ''; td { code @commit.author.date } } tr { td 'committer: '; td @commit.committer.name + ' <' + @commit.committer.email + '>'} tr { td ''; td { code @commit.committer.date } } tr { td 'commit sha: '; td { code @commit.sha } } tr do td 'tree sha: ' td do code { a @commit.gtree.sha, :href => R(Tree, @repo, @commit.gtree.sha) } span.space ' ' a 'archive', :href => R(Archive, @repo, @commit.gtree.sha) end end tr do td 'parents: ' td do @commit.parents.each do |p| code { a p.sha, :href => R(Commit, @repo, p.sha) } span.space ' ' a 'diff', :href => R(Diff, @repo, p.sha, @commit.sha) span.space ' ' a 'archive', :href => R(Archive, @repo, p.gtree.sha) br end end end end h3 'commit message' p @commit.message end def tree a.options 'repo', :href => R(View, @repo) h3 'tree : ' + @tree.sha p { a 'archive tree', :href => R(Archive, @repo, @tree.sha) }; table do @tree.children.each do |file, node| tr :class => cycle('odd','even') do td { code node.sha[0, 8] } td node.mode td file if node.type == 'tree' td { a node.type, :href => R(Tree, @repo, node.sha) } td { a 'archive', :href => R(Archive, @repo, node.sha) } else td { a node.type, :href => R(Blob, @repo, file, node.sha) } td { a 'raw', :href => R(BlobRaw, @repo, node.sha) } end end end end end def blob ext = File.extname(@file).gsub('.', '') case ext when 'rb' : classnm = 'sh_ruby' when 'js' : classnm = 'sh_javascript' when 'html' : classnm = 'sh_html' when 'css' : classnm = 'sh_css' end a.options 'repo', :href => R(View, @repo) h3 'blob : ' + @blob.sha h4 @file a 'download file', :href => R(Download, @repo, @file, @blob.sha) div.indent { pre @blob.contents, :class => classnm } end def diff a.options 'repo', :href => R(View, @repo) h1 "diff" p { a 'download patch file', :href => R(Patch, @repo, @tree1, @tree2) } p do a @tree1, :href => R(Tree, @repo, @tree1) span.space ' : ' a @tree2, :href => R(Tree, @repo, @tree2) end @diff.each do |file| h3 file.path div.indent { pre file.patch, :class => 'sh_diff' } end end # repo management views def add _form(@repo) end def _form(repo) form(:method => 'post') do label 'Path', :for => 'repo_path'; br input :name => 'repo_path', :type => 'text', :value => repo.path; br label 'Name', :for => 'repo_name'; br input :name => 'repo_name', :type => 'text', :value => repo.name; br label 'Bare', :for => 'repo_bare'; br input :type => 'checkbox', :name => 'repo_bare', :value => repo.bare; br input :type => 'hidden', :name => 'repo_id', :value => repo.id input :type => 'submit' end end def index @repos.each do | repo | h1 repo.name a 'remove', :href => R(RemoveRepo, repo.id) span.space ' ' a repo.path, :href => R(View, repo.id) end br br a 'add new repo', :href => R(Add) end # convenience functions def cycle(v1, v2) (@value == v1) ? @value = v2 : @value = v1 @value end end def GitWeb.create GitWeb::Models.create_schema end # everything below this line is the css and javascript for syntax-highlighting __END__ pre.sh_sourceCode { background-color: white; color: black; font-style: normal; font-weight: normal; } pre.sh_sourceCode .sh_keyword { color: blue; font-weight: bold; } /* language keywords */ pre.sh_sourceCode .sh_type { color: darkgreen; } /* basic types */ pre.sh_sourceCode .sh_string { color: red; font-family: monospace; } /* strings and chars */ pre.sh_sourceCode .sh_regexp { color: orange; font-family: monospace; } /* regular expressions */ pre.sh_sourceCode .sh_specialchar { color: pink; font-family: monospace; } /* e.g., \n, \t, \\ */ pre.sh_sourceCode .sh_comment { color: brown; font-style: italic; } /* comments */ pre.sh_sourceCode .sh_number { color: purple; } /* literal numbers */ pre.sh_sourceCode .sh_preproc { color: darkblue; font-weight: bold; } /* e.g., #include, import */ pre.sh_sourceCode .sh_symbol { color: darkred; } /* e.g., <, >, + */ pre.sh_sourceCode .sh_function { color: black; font-weight: bold; } /* function calls and declarations */ pre.sh_sourceCode .sh_cbracket { color: red; } /* block brackets (e.g., {, }) */ pre.sh_sourceCode .sh_todo { font-weight: bold; background-color: cyan; } /* TODO and FIXME */ /* for Perl, PHP, Prolog, Python, shell, Tcl */ pre.sh_sourceCode .sh_variable { color: darkgreen; } /* line numbers (not yet implemented) */ pre.sh_sourceCode .sh_linenum { color: black; font-family: monospace; } /* Internet related */ pre.sh_sourceCode .sh_url { color: blue; text-decoration: underline; font-family: monospace; } /* for ChangeLog and Log files */ pre.sh_sourceCode .sh_date { color: blue; font-weight: bold; } pre.sh_sourceCode .sh_time, pre.sh_sourceCode .sh_file { color: darkblue; font-weight: bold; } pre.sh_sourceCode .sh_ip, pre.sh_sourceCode .sh_name { color: darkgreen; } /* for LaTeX */ pre.sh_sourceCode .sh_italics { color: darkgreen; font-style: italic; } pre.sh_sourceCode .sh_bold { color: darkgreen; font-weight: bold; } pre.sh_sourceCode .sh_underline { color: darkgreen; text-decoration: underline; } pre.sh_sourceCode .sh_fixed { color: green; font-family: monospace; } pre.sh_sourceCode .sh_argument { color: darkgreen; } pre.sh_sourceCode .sh_optionalargument { color: purple; } pre.sh_sourceCode .sh_math { color: orange; } pre.sh_sourceCode .sh_bibtex { color: blue; } /* for diffs */ pre.sh_sourceCode .sh_oldfile { color: orange; } pre.sh_sourceCode .sh_newfile { color: darkgreen; } pre.sh_sourceCode .sh_difflines { color: blue; } /* for css */ pre.sh_sourceCode .sh_selector { color: purple; } pre.sh_sourceCode .sh_property { color: blue; } pre.sh_sourceCode .sh_value { color: darkgreen; font-style: italic; } __JS__ /* Copyright (C) 2007 gnombat@users.sourceforge.net */ /* License: http://shjs.sourceforge.net/doc/license.html */ function sh_highlightString(inputString,language,builder){var patternStack={_stack:[],getLength:function(){return this._stack.length;},getTop:function(){var stack=this._stack;var length=stack.length;if(length===0){return undefined;} return stack[length-1];},push:function(state){this._stack.push(state);},pop:function(){if(this._stack.length===0){throw"pop on empty stack";} this._stack.pop();}};var pos=0;var currentStyle=undefined;var output=function(s,style){var length=s.length;if(length===0){return;} if(!style){var pattern=patternStack.getTop();if(pattern!==undefined&&!('state'in pattern)){style=pattern.style;}} if(currentStyle!==style){if(currentStyle){builder.endElement();} if(style){builder.startElement(style);}} builder.text(s);pos+=length;currentStyle=style;};var endOfLinePattern=/\r\n|\r|\n/g;endOfLinePattern.lastIndex=0;var inputStringLength=inputString.length;while(posposWithinLine){output(line.substring(posWithinLine,bestMatch.index),null);} pattern=state[bestMatchIndex];var newStyle=pattern.style;var matchedString;if(newStyle instanceof Array){for(var subexpression=0;subexpression0){patternStack.pop();}}}}} if(currentStyle){builder.endElement();} currentStyle=undefined;if(endOfLineMatch){builder.text(endOfLineMatch[0]);} pos=startOfNextLine;}} function sh_getClasses(element){var result=[];var htmlClass=element.className;if(htmlClass&&htmlClass.length>0){var htmlClasses=htmlClass.split(" ");for(var i=0;i0){result.push(htmlClasses[i]);}}} return result;} function sh_addClass(element,name){var htmlClasses=sh_getClasses(element);for(var i=0;i0&&url.charAt(0)==='<'&&url.charAt(url.length-1)==='>'){url=url.substr(1,url.length-2);} if(sh_isEmailAddress(url)){url='mailto:'+url;} a.setAttribute('href',url);a.appendChild(this._document.createTextNode(this._currentText));this._currentParent.appendChild(a);} else{this._currentParent.appendChild(this._document.createTextNode(this._currentText));} this._currentText=null;} this._currentParent=this._currentParent.parentNode;},text:function(s){if(this._currentText===null){this._currentText=s;} else{this._currentText+=s;}},close:function(){if(this._currentText!==null){this._currentParent.appendChild(this._document.createTextNode(this._currentText));this._currentText=null;} this._element.appendChild(this._documentFragment);}};function sh_highlightElement(htmlDocument,element,language){sh_addClass(element,"sh_sourceCode");var inputString;if(element.childNodes.length===0){return;} else{inputString=sh_getText(element);} sh_builder.init(htmlDocument,element);sh_highlightString(inputString,language,sh_builder);sh_builder.close();} function sh_highlightHTMLDocument(htmlDocument){if(!window.sh_languages){return;} var nodeList=htmlDocument.getElementsByTagName("pre");for(var i=0;i element with class='"+htmlClass+"', but no such language exists";}}}}} function sh_highlightDocument(){sh_highlightHTMLDocument(document);} if(!this.sh_languages){this.sh_languages={};} sh_languages['css']=[[{'next':1,'regex':/\/\/\//g,'style':'sh_comment'},{'next':7,'regex':/\/\//g,'style':'sh_comment'},{'next':8,'regex':/\/\*\*/g,'style':'sh_comment'},{'next':14,'regex':/\/\*/g,'style':'sh_comment'},{'regex':/(?:\.|#)[A-Za-z0-9_]+/g,'style':'sh_selector'},{'next':15,'regex':/\{/g,'state':1,'style':'sh_cbracket'},{'regex':/~|!|%|\^|\*|\(|\)|-|\+|=|\[|\]|\\|:|;|,|\.|\/|\?|&|<|>|\|/g,'style':'sh_symbol'}],[{'exit':true,'regex':/$/g},{'regex':/(?:?)/g,'style':'sh_url'},{'regex':/(?:?)/g,'style':'sh_url'},{'next':2,'regex'://g,'style':'sh_keyword'},{'next':5,'regex':/<(?:\/)?[A-Za-z][A-Za-z0-9]*/g,'state':1,'style':'sh_keyword'},{'regex':/&(?:[A-Za-z0-9]+);/g,'style':'sh_preproc'},{'regex':/@[A-Za-z]+/g,'style':'sh_type'},{'regex':/(?:TODO|FIXME)(?:[:]?)/g,'style':'sh_todo'}],[{'exit':true,'regex':/>/g,'style':'sh_preproc'},{'next':3,'regex':/"/g,'style':'sh_string'}],[{'regex':/\\(?:\\|")/g},{'exit':true,'regex':/"/g,'style':'sh_string'}],[{'exit':true,'regex':/-->/g,'style':'sh_comment'},{'next':4,'regex'://g,'style':'sh_comment'},{'next':11,'regex'://g,'style':'sh_comment'},{'next':19,'regex'://g,'style':'sh_comment'},{'next':26,'regex'://g,'style':'sh_comment'},{'next':3,'regex'://g,'style':'sh_comment'},{'next':4,'regex'://g,'style':'sh_comment'},{'next':11,'regex':/