tests/run-tests.html: move noscript into body
[git-browser.git] / GitBrowser.js
blob91d2cf25bc8d124d50255275ac2ad16806a43a00
1 /*
2 Copyright (C) 2005, Artem Khodush <greenkaa@gmail.com>
4 This file is licensed under the GNU General Public License version 2.
5 */
7 if( typeof( GitBrowser )=="undefined" ) {
8 GitBrowser={};
9 GitBrowser._templates_url="templates.html?v="+Math.random();
12 if( typeof( InvisibleRequest )=="undefined" ) {
13 alert( "javascript file is omitted (InvisibleRequest.js) - this page will not work properly" );
16 // call_server
17 GitBrowser.set_error_handler=function( handler )
19 GitBrowser._user_error_handler=handler;
21 GitBrowser._user_error_handler=function( msg )
23 alert( msg );
25 GitBrowser._error_handler=function( msg, arg )
27 GitBrowser._user_error_handler( msg );
28 if( arg!=null ) {
29 ++arg.chain_i;
30 GitBrowser._next_call_server( arg );
31 }else {
32 arg.final_handler( arg.final_handler_arg );
35 GitBrowser._server_handler=function( doc, arg )
37 if( doc.error!=null ) {
38 GitBrowser._error_handler( doc.error, arg );
39 }else {
40 arg.handler( doc.result, arg.chain[arg.chain_i].handler_arg );
41 ++arg.chain_i;
42 GitBrowser._next_call_server( arg );
45 GitBrowser._url_adjust=function( url )
47 var base=location.protocol+"//"+location.host;
48 var urlparts=url.match( /^[A-Za-z+.-]+:\/\/[^\s\/]+(\/.*)$/ );
49 if( urlparts ) {
50 return base+urlparts[1];
51 }else {
52 return base+(url[0]=="/"?"":"/")+url;
55 cfg_gitweb_url=GitBrowser._url_adjust( cfg_gitweb_url );
56 cfg_browsercgi_url=GitBrowser._url_adjust( cfg_browsercgi_url );
57 if( typeof( cfg_home_url )=="undefined" ) {
58 cfg_home_url=null;
60 if( typeof( cfg_home_text )=="undefined" ) {
61 cfg_home_text=null;
63 if( typeof( cfg_bycommit_title )=="undefined" ) {
64 cfg_bycommit_title=null;
66 if( typeof( cfg_bydate_title )=="undefined" ) {
67 cfg_bydate_title=null;
69 if( cfg_home_url != null ) {
70 cfg_home_url=GitBrowser._url_adjust( cfg_home_url );
72 GitBrowser._g_server_url=cfg_browsercgi_url;
73 GitBrowser._g_server_timeout_seconds=132;
74 GitBrowser._make_server_url=function( arg )
76 var url=GitBrowser._g_server_url+"?";
77 url+="sub="+encodeURIComponent( arg.sub );
78 if( arg.repo!=null ) {
79 url+="&repo="+encodeURIComponent( arg.repo );
81 if( arg.sub_args!=null ) {
82 var sub_arg_name;
83 for( sub_arg_name in arg.sub_args ) {
84 var sub_arg=arg.sub_args[sub_arg_name];
85 for( var sub_i=0; sub_i<sub_arg.length; ++sub_i ) {
86 var value=sub_arg[sub_i];
87 if( value!=null && !value.match( /^\s*$/ ) ) {
88 url+="&"+sub_arg_name+"="+encodeURIComponent( value );
93 return url;
95 GitBrowser._next_call_server=function( arg )
97 if( arg.chain_i<arg.chain.length ) {
98 if( arg.before_handler!=null ) {
99 arg.before_handler( arg.chain[arg.chain_i].handler_arg );
101 InvisibleRequest.get( { url: GitBrowser._make_server_url( arg.chain[arg.chain_i] ),
102 handler: GitBrowser._server_handler,
103 handler_arg: arg,
104 error_handler: GitBrowser._error_handler,
105 timeout_seconds: GitBrowser._g_server_timeout_seconds
106 } );
107 }else {
108 arg.final_handler( arg.final_handler_arg );
111 // handler: handler
112 // before_handler: called before each server request
113 // final_handler: called when all requests are finished
114 // final_handler_arg: the only argument to the previous
115 // chain: array of { sub: repo: handler_arg: sub_args: }. if null, its assumed to be the one-element chain with the following items specified directly:
116 // sub: sub_name
117 // repo: repo_name (optional)
118 // handler_arg: second argument for handler
119 // sub_args: [array of sub arguments]
120 GitBrowser.call_server=function( arg )
122 var before_handler=arg.before_handler;
123 if( arg.before_handler==null ) {
124 before_handler=function() {};
126 var final_handler=arg.final_handler;
127 if( arg.final_handler==null ) {
128 final_handler=function() {};
130 var chain=arg.chain;
131 if( chain==null ) {
132 chain=[ { sub: arg.sub, repo: arg.repo, handler_arg: arg.handler_arg, sub_args: arg.sub_args } ];
134 GitBrowser._next_call_server( { chain: chain, chain_i: 0, handler: arg.handler, before_handler: before_handler, final_handler: final_handler, final_handler_arg: arg.final_handler_arg } );
138 // status_show, error_show
139 GitBrowser._g_status_div=null;
140 GitBrowser._g_error_div=null;
142 GitBrowser.setup_status_error=function()
144 var status=document.createElement( "DIV" );
145 status.style.display="none";
146 status.style.position="absolute";
147 status.style.top="0";
148 status.style.right="3em";
149 status.style.fontSize="10pt";
150 status.style.paddingTop="2px";
151 status.style.paddingBottom="2px";
152 status.style.paddingLeft="5px";
153 status.style.paddingRight="5px";
154 status.style.color="#ffffff";
155 status.style.backgroundColor="#090";
156 document.body.appendChild( status );
157 GitBrowser._g_status_div=status;
158 var error=document.createElement( "DIV" );
159 var error_close=document.createElement( "SPAN" );
160 error_close.appendChild( document.createTextNode( "close" ) );
161 error.appendChild( error_close );
162 error.appendChild( document.createElement( "SPAN" ) );
163 error.style.display="none";
164 error.style.border="1px solid #a00";
165 error.style.color="#800";
166 error.style.backgroundColor="#ffffff";
167 error.style.paddingTop="3px";
168 error.style.paddingBottom="3px";
169 error.style.paddingLeft="5px";
170 error.style.paddingRight="5px";
171 error.style.position="absolute";
172 error.style.top="3px";
173 error.style.left="3px";
174 error.style.zIndex="10";
175 error_close.style.color="#ffffff";
176 error_close.style.backgroundColor="#a22";
177 error_close.style.marginTop="3px";
178 error_close.style.marginBottom="3px";
179 error_close.style.marginLeft="1em";
180 error_close.style.marginRight="5px";
181 error_close.style.paddingTop="0";
182 error_close.style.paddingBottom="0";
183 error_close.style.paddingLeft="3px";
184 error_close.style.paddingRight="3px";
185 error_close.style.cursor="pointer";
186 error_close.onclick=GitBrowser.error_close;
187 document.body.appendChild( error );
188 GitBrowser._g_error_div=error;
189 GitBrowser.set_error_handler( GitBrowser.error_show );
191 GitBrowser.status_show=function( msg )
193 if( GitBrowser._g_status_div!=null ) {
194 if( msg!=null && msg!="" ) {
195 GitBrowser._g_status_div.innerHTML="";
196 GitBrowser._g_status_div.appendChild( document.createTextNode( msg ) );
197 GitBrowser._g_status_div.style.display="block";
198 }else {
199 GitBrowser._g_status_div.style.display="none";
203 GitBrowser.error_show=function( msg )
205 GitBrowser.status_show();
206 GitBrowser._g_error_div.lastChild.innerHTML="";
207 GitBrowser._g_error_div.lastChild.appendChild( document.createTextNode( "Error: "+msg ) );
208 GitBrowser._g_error_div.style.display="block";
210 GitBrowser.error_close=function()
212 GitBrowser._g_error_div.style.display="none";
215 // decode / encode selected repositories and refs as url parameters / text description
216 // repos={ repo_name => { all_heads: boolean, heads: [strings], tags: [strings] } }
217 GitBrowser.repos_decode_location=function( location )
219 var repos={};
220 var args=location.search;
221 if( args.charAt( 0 )=="?" ) {
222 args=args.slice( 1 );
224 if( args.length>0 ) {
225 args=args.split( "&" );
226 for( var arg_i=0; arg_i<args.length; ++arg_i ) {
227 var arg=args[arg_i].split( "=" );
228 if( arg[0]=="r" ) {
229 var repo_name=decodeURIComponent(arg[1]);
230 if( repos[repo_name]==null ) {
231 repos[repo_name]={ heads: [], tags: [] };
233 repos[repo_name].all_heads=true;
234 }else if( arg[0]=="h" || arg[0]=="t" ) {
235 var ref=arg[1].split( "," );
236 var repo_name=decodeURIComponent(ref[0]);
237 var ref_name=decodeURIComponent(ref[1]);
238 if( repos[repo_name]==null ) {
239 repos[repo_name]={ heads: [], tags: [] };
241 if( arg[0]=="h" ) {
242 repos[repo_name].heads.push( ref_name );
243 }else {
244 repos[repo_name].tags.push( ref_name );
249 return repos;
251 GitBrowser.repos_encode_url_param=function( repos )
253 var params=[];
254 for( var repo_name in repos ) {
255 var repo=repos[repo_name];
256 if( repo.all_heads ) {
257 params.push( "r="+encodeURIComponent( repo_name ) );
259 for( var head_i=0; head_i<repo.heads.length; ++head_i ) {
260 params.push( "h="+encodeURIComponent( repo_name )+","+encodeURIComponent( repo.heads[head_i] ) );
262 for( var tag_i=0; tag_i<repo.tags.length; ++tag_i ) {
263 params.push( "t="+encodeURIComponent( repo_name )+","+encodeURIComponent( repo.tags[tag_i] ) );
266 return params.join( "&" );
268 GitBrowser.repos_encode_text=function( repos )
270 var text=[];
271 for( var repo_name in repos ) {
272 var repo=repos[repo_name];
273 if( repo.all_heads ) {
274 text.push( "all "+repo_name+" heads" );
276 if( repo.heads.length>0 ) {
277 text.push( repo_name+" heads: "+repo.heads.join( " " ) );
279 if( repo.tags.length>0 ) {
280 text.push( repo_name+" tags: "+repo.tags.join( " " ) );
283 return text.join( "; " );
286 // filter dialog.
287 // global vars:
288 // dialog: HTML filter div element
289 // x, y: filter dialog absolute pos
290 // apply_handler: called when "reload" filter button is clicked. argument: { exclude_commits: [], paths: [] }
291 // apply_handler_context: second argument to apply_handler
292 // exclude_edit: HTML edit element for commits to exclude
293 // paths_edit: HTML edit element for paths to limit git-rev-list output
294 GitBrowser._g_filter={};
296 GitBrowser._filter_dialog_close=function()
298 GitBrowser._g_filter.dialog.style.display="none";
300 GitBrowser._filter_dialog_apply=function()
302 var exclude_commits=GitBrowser._g_filter.exclude_edit.value.split( " " );
303 var paths=GitBrowser._g_filter.paths_edit.value.split( " " );
304 GitBrowser._g_filter.dialog.style.display="none";
305 GitBrowser._g_filter.apply_handler( { exclude_commits: exclude_commits, paths: paths }, GitBrowser._g_filter.apply_handler_context );
307 GitBrowser._filter_dialog_clear=function()
309 GitBrowser._g_filter.exclude_edit.value="";
310 GitBrowser._g_filter.paths_edit.value="";
312 GitBrowser._filter_dialog_show=function()
314 if( GitBrowser._g_filter.dialog.style.display=="none" ) {
315 GitBrowser._g_filter.dialog.style.display="";
316 var y=GitBrowser._g_filter.y;
317 if( y>500 ) { // XXX it's random
318 y-=GitBrowser._g_filter.dialog.clientHeight;
320 Motion.set_page_coords( GitBrowser._g_filter.dialog, GitBrowser._g_filter.x, y );
321 }else {
322 GitBrowser._g_filter.dialog.style.display="none";
325 GitBrowser._filter_dialog_loaded=function( template, arg )
327 var data={
328 filterdialog: {
329 _process: function( n ) { GitBrowser._g_filter.dialog=n; },
330 filtertable: {
331 filterexclude: { _process: function( n ) { GitBrowser._g_filter.exclude_edit=n; } },
332 filterpath: { _process: function( n ) { GitBrowser._g_filter.paths_edit=n; } }
334 filterreload: { _process: function( n ) { n.onclick=GitBrowser._filter_dialog_apply; n.href="#"; } },
335 filterclear: { _process: function( n ) { n.onclick=GitBrowser._filter_dialog_clear; n.href="#"; } },
336 filterclose: { _process: function( n ) { n.onclick=GitBrowser._filter_dialog_close; n.href="#"; } }
339 DomTemplate.apply( template, data, document.body );
340 GitBrowser._g_filter.x=arg.x;
341 GitBrowser._g_filter.y=arg.y;
342 GitBrowser._g_filter.apply_handler=arg.apply_handler;
343 GitBrowser._g_filter.apply_handler_context=arg.apply_handler_context;
344 arg.show_button.onclick=GitBrowser._filter_dialog_show;
346 // arg:
347 // show_button: its onclick will show filter
348 // x, y: filter dialog pos
349 // apply_handler: called when "reload" filter button is clicked. argument: { exclude_commits: [], paths: [] }
350 GitBrowser.filter_dialog_init=function( arg )
352 InvisibleRequest.get_element( { url: GitBrowser._templates_url, element_id: "filterdialogtemplate",
353 handler: GitBrowser._filter_dialog_loaded, handler_arg: arg,
354 error_handler: GitBrowser.error_show } );
358 // title
359 GitBrowser._g_title={};
360 GitBrowser._title_loaded=function( template, arg )
362 var selected_text=GitBrowser.repos_encode_text( arg.repos );
363 if( selected_text=="" ) {
364 selected_text="none selected";
366 var data={
367 title: {
368 _process: function( n ) { arg.title_div=n; },
369 selectedtext: selected_text,
370 selectother: { _process: function( n ) { arg.select_other_btn=n } },
371 commitcount: { _process: function( n ) { GitBrowser._g_title.commitcount=n; } },
372 loadmore: { _process: function( n, context ) { GitBrowser._g_title.loadmore=n; arg.load_more_button_init( n ); }, _process_arg: arg },
373 filtershow: { _process: function( n, context ) { n.href="#"; arg.filter_button_init( n, context ); }, _process_arg: arg },
374 home_btn: { _process: function( n, context ) { n.removeAttribute( "id" ); GitBrowser._home_btn_init( n, context ) }, _process_arg: arg }
377 DomTemplate.apply( template, data, document.body );
378 if( arg.title_loaded_handler!=null ) {
379 arg.title_loaded_handler( arg );
381 arg.exclude_commits=[];
382 arg.paths=[];
383 GitBrowser.commits_load_first( arg );
385 GitBrowser._home_btn_init=function( n, context )
387 if( cfg_home_url != null ) {
388 var url=cfg_home_url;
389 var repo="";
390 if ( context.repos != null && typeof( context.repos )=="object" ) {
391 for ( var k in context.repos ) {
392 repo = k;
393 break;
396 url=url.replace( /%n/g, encodeURIComponent(repo) );
397 url=url.replace( /%2[bB]/g, "+" );
398 url=url.replace( /%2[fF]/g, "/" );
399 n.href=url;
401 if( cfg_home_text != null ) {
402 var text_node=n.ownerDocument.createTextNode( cfg_home_text );
403 var first_child=n.firstChild;
404 if( first_child == null ) {
405 n.appendChild( text_node );
406 }else {
407 if( first_child.nodeType==3 ) { // it's a text node
408 n.removeChild( first_child );
409 first_child=n.firstChild;
410 if( first_child == null ) {
411 n.appendChild( text_node );
412 }else {
413 n.insertBefore( text_node, first_child );
415 }else {
416 n.insertBefore( text_node, first_child );
422 // arg:
423 // load_more_button_init: function( b )
424 // filter_button_init: function( b )
425 // title_loaded_handler: called when the title is loaded into the document, takes title_div as an argument
426 // commits_first_loaded_handler: function( context )
427 // commits_more_loaded_handler: function( context )
428 // context: {
429 // diagram: GitDiagram object
430 // diagram_div:
431 // repos: as returned by repos_decode_location
432 // (assigned later)
433 // title_div:
434 // exclude_commits: []
435 // paths: []
436 // }
437 GitBrowser.title_init=function( arg )
439 var title = arg.title;
440 if ( title != null && title != "" ) {
441 var repo="";
442 if ( arg.repos != null && typeof( arg.repos )=="object" ) {
443 for ( var k in arg.repos ) {
444 repo = k;
445 break;
448 title=title.replace( /%n/g, repo );
449 document.title=title;
451 InvisibleRequest.get_element( { url: GitBrowser._templates_url, element_id: "titletemplate",
452 handler: GitBrowser._title_loaded, handler_arg: arg,
453 error_handler: GitBrowser.error_show } );
455 //arg:
456 // diagram: diagram
457 GitBrowser.title_update=function( arg )
459 GitBrowser._g_title.commitcount.innerHTML="";
460 var cmtcount = arg.diagram.get_commit_count();
461 var cmtpl = (cmtcount != 1) ? "s" : "";
462 GitBrowser._g_title.commitcount.appendChild( document.createTextNode( "Loaded "+cmtcount+" commit"+cmtpl+" " ) );
463 var need_more=arg.diagram.get_start_more_ids().length!=0;
464 GitBrowser._g_title.loadmore.style.visibility= need_more ? "visible" : "hidden";
467 // diagram loading (calls only add_node)
468 GitBrowser._add_refs_and_commits=function( data, arg )
470 if ( data.master!=null && data.master.length>0 ) {
471 arg.diagram.add_master( arg.repo_name, data.master );
473 for( var i=0; i<data.refs.length; ++i ) {
474 arg.diagram.add_label( data.refs[i].id, data.refs[i].name, arg.repo_name, data.refs[i].type );
476 GitBrowser._add_commits( data.commits, arg );
478 GitBrowser._add_commits=function( commits, arg )
480 var tmp=[];
481 for( var commit_id in commits ) {
482 tmp.push( commit_id );
484 tmp.sort();
485 for( var tmp_i=0; tmp_i<tmp.length; ++tmp_i ) {
486 var commit=commits[tmp[tmp_i]];
487 if( (commit.committer_epoch!=null || commit.author_epoch!=null) && commit.id!=null && commit.author!=null && commit.parents!=null ) {
488 var committer_time=commit.committer_epoch==null ? null : commit.committer_epoch*1000;
489 var author_time=commit.author_epoch==null ? null : commit.author_epoch*1000;
490 var comment=commit.comment==null ? "" : commit.comment.join( " " );
491 arg.diagram.add_node( commit.id, committer_time, author_time, commit.author, comment, commit.parents, arg.repo_name );
495 // arg:
496 // repos: as returned by repos_decode_location
497 // diagram: diagram
498 // exclude_commits: [], as passed to apply_handler first arg in filter_dialog
499 // paths: [], as passed to apply_handler first arg in filter_dialog
500 // commits_first_loaded_handler: function( arg )
501 GitBrowser.commits_load_first=function( arg )
503 var chain=[];
504 for( var repo_name in arg.repos ) {
505 var repo=arg.repos[repo_name];
506 var refs=[];
507 if( repo.all_heads ) {
508 refs.push( "r,all" );
510 for( var head_i=0; head_i<repo.heads.length; ++head_i ) {
511 refs.push( "h,"+repo.heads[head_i] );
513 for( var tag_i=0; tag_i<repo.tags.length; ++tag_i ) {
514 refs.push( "t,"+repo.tags[tag_i] );
516 refs.sort();
517 chain.push( { sub: "commits_from_refs", repo: repo_name, handler_arg: { diagram: arg.diagram, repo_name: repo_name },
518 sub_args: { ref: refs, x: arg.exclude_commits, path: arg.paths, shortcomment: [arg.shortcomment] }
519 } );
522 GitBrowser.status_show( "loading..." );
523 GitBrowser.call_server( { handler: GitBrowser._add_refs_and_commits,
524 final_handler: function( arg ) { GitBrowser.status_show( "" ); arg.commits_first_loaded_handler( arg ); }, final_handler_arg: arg,
525 chain: chain } );
527 // arg:
528 // diagram: diagram
529 // exclude_commits: [], as passed to apply_handler first arg in filter_dialog
530 // paths: [], as passed to apply_handler first arg in filter_dialog
531 // commits_more_loaded_handler: function( arg )
532 GitBrowser.commits_load_more=function( arg )
534 var repo_map={};
535 var more_ids=arg.diagram.get_start_more_ids();
536 for( var i=0; i<more_ids.length; ++i ) {
537 var id=more_ids[i];
538 for( var repo_i=0; repo_i<id.repos.length; ++repo_i ) {
539 var repo_name=id.repos[repo_i];
540 if( repo_map[repo_name]==null ) {
541 repo_map[repo_name]=[[]];
543 var ids=repo_map[repo_name][repo_map[repo_name].length-1];
544 if( ids.length>9 ) { // split to avoid too long urls - for now, the limit is 10 40-byte ids per url.
545 // since server does not keep track which commits were already sent to which client,
546 // splitting requests may cause redundant data to be transferred.
547 repo_map[repo_name].push( [] );
548 ids=repo_map[repo_name][repo_map[repo_name].length-1];
550 ids.push( id.id );
553 var chain=[];
554 for( var repo_name in repo_map ) {
555 var ids_a=repo_map[repo_name];
556 for( var i=0; i<ids_a.length; ++i ) {
557 var ids=ids_a[i];
558 ids.sort();
559 chain.push( { sub: "commits_from_ids", repo: repo_name, handler_arg: { diagram: arg.diagram, repo_name: repo_name },
560 sub_args: { id: ids, x: arg.exclude_commits, path: arg.paths, shortcomment: [arg.shortcomment] }
561 } );
564 GitBrowser.status_show( "loading..." );
565 GitBrowser.call_server( { handler: GitBrowser._add_commits,
566 final_handler: function( arg ) { GitBrowser.status_show( "" ); arg.commits_more_loaded_handler( arg ); }, final_handler_arg: arg,
567 chain: chain } );
570 // glue code that appears to be common between by-date.html and by-commits.html
571 // diagram ui handler. first argument should be ui handlers map: event_name=>handler
572 GitBrowser.diagram_ui_handler=function()
574 var ui_map=arguments[0];
575 var event_name=arguments[1];
576 var args=[];
577 for( var i=2; i<arguments.length; ++i ) {
578 args.push(arguments[i] );
580 var handler=ui_map[event_name];
581 if( handler!=null ) {
582 handler.apply( this, args );
585 // filter
586 GitBrowser.filter_dialog_handler=function( arg, context )
588 context.exclude_commits=arg.exclude_commits;
589 context.paths=arg.paths;
590 context.diagram.clear();
591 GitBrowser.commits_load_first( context );
593 GitBrowser.filter_dialog_create=function( filter_button, context )
595 var ref_pos=Motion.get_page_coords( filter_button );
596 var x=ref_pos.x+filter_button.clientWidth;
597 var y=ref_pos.y+2+filter_button.scrollHeight;
598 GitBrowser.filter_dialog_init( { show_button: filter_button, x: x, y: y, apply_handler: GitBrowser.filter_dialog_handler, apply_handler_context: context } );
601 // arg:
602 // repos: as as returned by repos_decode_location
603 // diagram: GitDiagram object
604 // title_loaded_handler
605 // commits_first_loaded_handler
606 // commits_more_loaded_handler
607 GitBrowser.init=function( arg )
609 GitBrowser.setup_status_error();
610 arg.load_more_button_init=function( b ) { b.href="#"; b.onclick=function() { GitBrowser.commits_load_more( arg ) }; };
611 arg.filter_button_init=GitBrowser.filter_dialog_create;
612 GitBrowser.title_init( arg );