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 );
33         }
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 );
43         }
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;
53         }
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 );
80         }
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 );
89                                 }
90                         }
91                 }
92         }
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 );
100                 }
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 );
109         }
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() {};
125         }
126         var final_handler=arg.final_handler;
127         if( arg.final_handler==null ) {
128                 final_handler=function() {};
129         }
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 } ];
133         }
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";
200                 }
201         }
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 );
223         }
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: [] };
232                                 }
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: [] };
240                                 }
241                                 if( arg[0]=="h" ) {
242                                         repos[repo_name].heads.push( ref_name );
243                                 }else {
244                                         repos[repo_name].tags.push( ref_name );
245                                 }
246                         }
247                 }
248         }
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 ) );
258                 }
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] ) );
261                 }
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] ) );
264                 }
265         }
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" );
275                 }
276                 if( repo.heads.length>0 ) {
277                         text.push( repo_name+" heads: "+repo.heads.join( " " ) );
278                 }
279                 if( repo.tags.length>0 ) {
280                         text.push( repo_name+" tags: "+repo.tags.join( " " ) );
281                 }
282         }
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;
319                 }
320                 Motion.set_page_coords( GitBrowser._g_filter.dialog, GitBrowser._g_filter.x, y );
321         }else {
322                 GitBrowser._g_filter.dialog.style.display="none";
323         }
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; } }
333                         },
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="#"; } }
337                 }
338         };
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";
365         }
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 }
375                 }
376         };
377         DomTemplate.apply( template, data, document.body );
378         if( arg.title_loaded_handler!=null ) {
379                 arg.title_loaded_handler( arg );
380         }
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;
394                         }
395                 }
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;
400         }
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 );
414                                 }
415                         }else {
416                                 n.insertBefore( text_node, first_child );
417                         }
418                 }
419         }
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;
446                         }
447                 }
448                 title=title.replace( /%n/g, repo );
449                 document.title=title;
450         }
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 );
472         }
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 );
475         }
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 );
483         }
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 );
492                 }
493         }
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" );
509                 }
510                 for( var head_i=0; head_i<repo.heads.length; ++head_i ) {
511                         refs.push( "h,"+repo.heads[head_i] );
512                 }
513                 for( var tag_i=0; tag_i<repo.tags.length; ++tag_i ) {
514                         refs.push( "t,"+repo.tags[tag_i] );
515                 }
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                         } );
520         }
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]=[[]];
542                         }
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];
549                         }
550                         ids.push( id.id );
551                 }
552         }
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                                 } );
562                 }
563         }
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] );
579         }
580         var handler=ui_map[event_name];
581         if( handler!=null ) {
582                 handler.apply( this, args );
583         }
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 );