git-browser.cgi: fix handling of latin-1 characters
[git-browser-mirror.git] / GitBrowser.js
blobe1e58bf283071fb15aa82601db703b4d84a8db6a
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={};
11 if( typeof( InvisibleRequest )=="undefined" ) {
12         alert( "javascript file is omitted (InvisibleRequest.js) - this page will not work properly" );
15 // call_server
16 GitBrowser.set_error_handler=function( handler )
18         GitBrowser._user_error_handler=handler;
20 GitBrowser._user_error_handler=function( msg )
22         alert( msg );
24 GitBrowser._error_handler=function( msg, arg )
26         GitBrowser._user_error_handler( msg );
27         if( arg!=null ) {
28                 ++arg.chain_i;
29                 GitBrowser._next_call_server( arg );
30         }else {
31                 arg.final_handler( arg.final_handler_arg );
32         }
34 GitBrowser._server_handler=function( doc, arg )
36         if( doc.error!=null ) {
37                 GitBrowser._error_handler( doc.error, arg );
38         }else {
39                 arg.handler( doc.result, arg.chain[arg.chain_i].handler_arg );
40                 ++arg.chain_i;
41                 GitBrowser._next_call_server( arg );
42         }
44 GitBrowser._url_adjust=function( url )
46         var base=location.protocol+"//"+location.host;
47         var urlparts=url.match( /^[A-Za-z+.-]+:\/\/[^\s\/]+(\/.*)$/ );
48         if( urlparts ) {
49                 return base+urlparts[1];
50         }else {
51                 return base+(url[0]=="/"?"":"/")+url;
52         }
54 cfg_gitweb_url=GitBrowser._url_adjust( cfg_gitweb_url );
55 cfg_browsercgi_url=GitBrowser._url_adjust( cfg_browsercgi_url );
56 GitBrowser._g_server_url=cfg_browsercgi_url;
57 GitBrowser._g_server_timeout_seconds=132;
58 GitBrowser._make_server_url=function( arg )
60         var url=GitBrowser._g_server_url+"?";
61         url+="sub="+encodeURIComponent( arg.sub );
62         if( arg.repo!=null ) {
63                 url+="&repo="+encodeURIComponent( arg.repo );
64         }
65         if( arg.sub_args!=null ) {
66                 var sub_arg_name;
67                 for( sub_arg_name in arg.sub_args ) {
68                         var sub_arg=arg.sub_args[sub_arg_name];
69                         for( var sub_i=0; sub_i<sub_arg.length; ++sub_i ) {
70                                 var value=sub_arg[sub_i];
71                                 if( value!=null && !value.match( /^\s*$/ ) ) {
72                                         url+="&"+sub_arg_name+"="+encodeURIComponent( value );
73                                 }
74                         }
75                 }
76         }
77         return url;
79 GitBrowser._next_call_server=function( arg )
81         if( arg.chain_i<arg.chain.length ) {
82                 if( arg.before_handler!=null ) {
83                         arg.before_handler( arg.chain[arg.chain_i].handler_arg );
84                 }
85                 InvisibleRequest.get( { url: GitBrowser._make_server_url( arg.chain[arg.chain_i] ),
86                                         handler: GitBrowser._server_handler,
87                                         handler_arg: arg,
88                                         error_handler: GitBrowser._error_handler,
89                                         timeout_seconds: GitBrowser._g_server_timeout_seconds
90                 } );
91         }else {
92                 arg.final_handler( arg.final_handler_arg );
93         }
95 // handler: handler
96 // before_handler: called before each server request
97 // final_handler: called when all requests are finished
98 // final_handler_arg: the only argument to the previous
99 // 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:
100 // sub: sub_name
101 // repo: repo_name (optional)
102 // handler_arg: second argument for handler
103 // sub_args: [array of sub arguments]
104 GitBrowser.call_server=function( arg )
106         var chain=arg.chain;
107         if( chain==null ) {
108                 chain=[ { sub: arg.sub, repo: arg.repo, handler_arg: arg.handler_arg, sub_args: arg.sub_args } ];
109         }
110         GitBrowser._next_call_server( { chain: chain, chain_i: 0, handler: arg.handler, before_handler: arg.before_handler, final_handler: arg.final_handler, final_handler_arg: arg.final_handler_arg } );
114 // status_show, error_show
115 GitBrowser._g_status_div=null;
116 GitBrowser._g_error_div=null;
118 GitBrowser.setup_status_error=function()
120         var status=document.createElement( "DIV" );
121         status.style.display="none";
122         status.style.position="absolute";
123         status.style.top="0";
124         status.style.right="3em";
125         status.style.fontSize="10pt";
126         status.style.paddingTop="2px";
127         status.style.paddingBottom="2px";
128         status.style.paddingLeft="5px";
129         status.style.paddingRight="5px";
130         status.style.color="#ffffff";
131         status.style.backgroundColor="#090";
132         document.body.appendChild( status );
133         GitBrowser._g_status_div=status;
134         var error=document.createElement( "DIV" );
135         var error_close=document.createElement( "SPAN" );
136         error_close.appendChild( document.createTextNode( "close" ) );
137         error.appendChild( error_close );
138         error.appendChild( document.createElement( "SPAN" ) );
139         error.style.display="none";
140         error.style.border="1px solid #a00";
141         error.style.color="#800";
142         error.style.backgroundColor="#ffffff";
143         error.style.paddingTop="3px";
144         error.style.paddingBottom="3px";
145         error.style.paddingLeft="5px";
146         error.style.paddingRight="5px";
147         error.style.position="absolute";
148         error.style.top="3px";
149         error.style.left="3px";
150         error.style.zIndex="10";
151         error_close.style.color="#ffffff";
152         error_close.style.backgroundColor="#a22";
153         error_close.style.marginTop="3px";
154         error_close.style.marginBottom="3px";
155         error_close.style.marginLeft="1em";
156         error_close.style.marginRight="5px";
157         error_close.style.paddingTop="0";
158         error_close.style.paddingBottom="0";
159         error_close.style.paddingLeft="3px";
160         error_close.style.paddingRight="3px";
161         error_close.style.cursor="pointer";
162         error_close.onclick=GitBrowser.error_close;
163         document.body.appendChild( error );
164         GitBrowser._g_error_div=error;
165         GitBrowser.set_error_handler( GitBrowser.error_show );
167 GitBrowser.status_show=function( msg )
169         if( GitBrowser._g_status_div!=null ) {
170                 if( msg!=null && msg!="" ) {
171                         GitBrowser._g_status_div.innerHTML="";
172                         GitBrowser._g_status_div.appendChild( document.createTextNode( msg ) );
173                         GitBrowser._g_status_div.style.display="block";
174                 }else {
175                         GitBrowser._g_status_div.style.display="none";
176                 }
177         }
179 GitBrowser.error_show=function( msg )
181         GitBrowser.status_show();
182         GitBrowser._g_error_div.lastChild.innerHTML="";
183         GitBrowser._g_error_div.lastChild.appendChild( document.createTextNode( "Error: "+msg ) );
184         GitBrowser._g_error_div.style.display="block";
186 GitBrowser.error_close=function()
188         GitBrowser._g_error_div.style.display="none";
191 // decode / encode selected repositories and refs as url parameters / text description
192 // repos={ repo_name => { all_heads: boolean, heads: [strings], tags: [strings] } }
193 GitBrowser.repos_decode_location=function( location )
195         var repos={};
196         var args=location.search;
197         if( args.charAt( 0 )=="?" ) {
198                 args=args.slice( 1 );
199         }
200         if( args.length>0 ) {
201                 args=args.split( "&" );
202                 for( var arg_i=0; arg_i<args.length; ++arg_i ) {
203                         var arg=args[arg_i].split( "=" );
204                         if( arg[0]=="r" ) {
205                                 var repo_name=arg[1];
206                                 if( repos[repo_name]==null ) {
207                                         repos[repo_name]={ heads: [], tags: [] };
208                                 }
209                                 repos[repo_name].all_heads=true;
210                         }else if( arg[0]=="h" || arg[0]=="t" ) {
211                                 var ref=arg[1].split( "," );
212                                 var repo_name=ref[0];
213                                 var ref_name=ref[1];
214                                 if( repos[repo_name]==null ) {
215                                         repos[repo_name]={ heads: [], tags: [] };
216                                 }
217                                 if( arg[0]=="h" ) {
218                                         repos[repo_name].heads.push( ref_name );
219                                 }else {
220                                         repos[repo_name].tags.push( ref_name );
221                                 }
222                         }
223                 }
224         }
225         return repos;
227 GitBrowser.repos_encode_url_param=function( repos )
229         var params=[];
230         for( var repo_name in repos ) {
231                 var repo=repos[repo_name];
232                 if( repo.all_heads ) {
233                         params.push( "r="+encodeURIComponent( repo_name ) );
234                 }
235                 for( var head_i=0; head_i<repo.heads.length; ++head_i ) {
236                         params.push( "h="+encodeURIComponent( repo_name )+","+encodeURIComponent( repo.heads[head_i] ) );
237                 }
238                 for( var tag_i=0; tag_i<repo.tags.length; ++tag_i ) {
239                         params.push( "t="+encodeURIComponent( repo_name )+","+encodeURIComponent( repo.tags[tag_i] ) );
240                 }
241         }
242         return params.join( "&" );
244 GitBrowser.repos_encode_text=function( repos )
246         var text=[];
247         for( var repo_name in repos ) {
248                 var repo=repos[repo_name];
249                 if( repo.all_heads ) {
250                         text.push( "all "+repo_name+" heads" );
251                 }
252                 if( repo.heads.length>0 ) {
253                         text.push( repo_name+" heads: "+repo.heads.join( " " ) );
254                 }
255                 if( repo.tags.length>0 ) {
256                         text.push( repo_name+" tags: "+repo.tags.join( " " ) );
257                 }
258         }
259         return text.join( "; " );
262 // filter dialog.
263 // global vars:
264 // dialog: HTML filter div element
265 // x, y: filter dialog absolute pos
266 // apply_handler: called when "reload" filter button is clicked. argument: { exclude_commits: [], paths: [] }
267 // apply_handler_context: second argument to apply_handler
268 // exclude_edit: HTML edit element for commits to exclude
269 // paths_edit: HTML edit element for paths to limit git-rev-list output
270 GitBrowser._g_filter={};
272 GitBrowser._filter_dialog_close=function()
274         GitBrowser._g_filter.dialog.style.display="none";
276 GitBrowser._filter_dialog_apply=function()
278         var exclude_commits=GitBrowser._g_filter.exclude_edit.value.split( " " );
279         var paths=GitBrowser._g_filter.paths_edit.value.split( " " );
280         GitBrowser._g_filter.dialog.style.display="none";
281         GitBrowser._g_filter.apply_handler( { exclude_commits: exclude_commits, paths: paths }, GitBrowser._g_filter.apply_handler_context );
283 GitBrowser._filter_dialog_clear=function()
285         GitBrowser._g_filter.exclude_edit.value="";
286         GitBrowser._g_filter.paths_edit.value="";
288 GitBrowser._filter_dialog_show=function()
290         if( GitBrowser._g_filter.dialog.style.display=="none" ) {
291                 GitBrowser._g_filter.dialog.style.display="";
292                 var y=GitBrowser._g_filter.y;
293                 if( y>500 ) { // XXX it's random
294                         y-=GitBrowser._g_filter.dialog.clientHeight;
295                 }
296                 Motion.set_page_coords( GitBrowser._g_filter.dialog, GitBrowser._g_filter.x, y );
297         }else {
298                 GitBrowser._g_filter.dialog.style.display="none";
299         }
301 GitBrowser._filter_dialog_loaded=function( template, arg )
303         var data={
304                 filterdialog: {
305                         _process: function( n ) { GitBrowser._g_filter.dialog=n; },
306                         filtertable: {
307                                 filterexclude: { _process: function( n ) { GitBrowser._g_filter.exclude_edit=n; } },
308                                 filterpath: { _process: function( n ) { GitBrowser._g_filter.paths_edit=n; } }
309                         },
310                         filterreload: { _process: function( n ) { n.onclick=GitBrowser._filter_dialog_apply; n.href="#"; } },
311                         filterclear: { _process: function( n ) { n.onclick=GitBrowser._filter_dialog_clear; n.href="#"; } },
312                         filterclose: { _process: function( n ) { n.onclick=GitBrowser._filter_dialog_close; n.href="#"; } }
313                 }
314         };
315         DomTemplate.apply( template, data, document.body );
316         GitBrowser._g_filter.x=arg.x;
317         GitBrowser._g_filter.y=arg.y;
318         GitBrowser._g_filter.apply_handler=arg.apply_handler;
319         GitBrowser._g_filter.apply_handler_context=arg.apply_handler_context;
320         arg.show_button.onclick=GitBrowser._filter_dialog_show;
322 // arg:
323 //      show_button: its onclick will show filter
324 //      x, y: filter dialog pos
325 //      apply_handler: called when "reload" filter button is clicked. argument: { exclude_commits: [], paths: [] }
326 GitBrowser.filter_dialog_init=function( arg )
328         InvisibleRequest.get_element( { url: "templates.html", element_id: "filterdialogtemplate",
329                                         handler: GitBrowser._filter_dialog_loaded, handler_arg: arg,
330                                         error_handler: GitBrowser.error_show } );
334 // title
335 GitBrowser._g_title={};
336 GitBrowser._title_loaded=function( template, arg )
338         var selected_text=GitBrowser.repos_encode_text( arg.repos );
339         if( selected_text=="" ) {
340                 selected_text="none selected";
341         }
342         var data={
343                 title: {
344                         _process: function( n ) { arg.title_div=n; },
345                         selectedtext: selected_text,
346                         selectother: { _process: function( n ) { arg.select_other_btn=n } },
347                         commitcount: { _process: function( n ) { GitBrowser._g_title.commitcount=n; } },
348                         loadmore: { _process: function( n, context ) { GitBrowser._g_title.loadmore=n; arg.load_more_button_init( n ); }, _process_arg: arg },
349                         filtershow: { _process: function( n, context ) { n.href="#"; arg.filter_button_init( n, context ); }, _process_arg: arg }
350                 }
351         };
352         DomTemplate.apply( template, data, document.body );
353         if( arg.title_loaded_handler!=null ) {
354                 arg.title_loaded_handler( arg );
355         }
356         arg.exclude_commits=[];
357         arg.paths=[];
358         GitBrowser.commits_load_first( arg );
361 // arg:
362 //      load_more_button_init: function( b )
363 //      filter_button_init: function( b )
364 //      title_loaded_handler: called when the title is loaded into the document, takes title_div as an argument
365 //      commits_first_loaded_handler: function( context )
366 //      commits_more_loaded_handler: function( context )
367 //      context: {
368 //              diagram: GitDiagram object
369 //              diagram_div:
370 //              repos: as returned by repos_decode_location
371 //              (assigned later)
372 //              title_div:
373 //              exclude_commits: []
374 //              paths: []
375 //      }
376 GitBrowser.title_init=function( arg )
378         InvisibleRequest.get_element( { url: "templates.html", element_id: "titletemplate",
379                                         handler: GitBrowser._title_loaded, handler_arg: arg,
380                                         error_handler: GitBrowser.error_show } );
382 //arg:
383 //      diagram: diagram
384 GitBrowser.title_update=function( arg )
386         GitBrowser._g_title.commitcount.innerHTML="";
387         GitBrowser._g_title.commitcount.appendChild( document.createTextNode( "Loaded "+arg.diagram.get_commit_count()+" commits " ) );
388         var need_more=arg.diagram.get_start_more_ids().length!=0;
389         GitBrowser._g_title.loadmore.style.visibility= need_more ? "visible" : "hidden";
392 // diagram loading (calls only add_node)
393 GitBrowser._add_refs_and_commits=function( data, arg )
395         for( var i=0; i<data.refs.length; ++i ) {
396                 arg.diagram.add_label( data.refs[i].id, data.refs[i].name, arg.repo_name, data.refs[i].type );
397         }
398         GitBrowser._add_commits( data.commits, arg );
400 GitBrowser._add_commits=function( commits, arg )
402         var tmp=[];
403         for( var commit_id in commits ) {
404                 tmp.push( commit_id );
405         }
406         tmp.sort();
407         for( var tmp_i=0; tmp_i<tmp.length; ++tmp_i ) {
408                 var commit=commits[tmp[tmp_i]];
409                 if( (commit.committer_epoch!=null || commit.author_epoch!=null) && commit.id!=null && commit.author!=null && commit.parents!=null ) {
410                         var committer_time=commit.committer_epoch==null ? null : commit.committer_epoch*1000;
411                         var author_time=commit.author_epoch==null ? null : commit.author_epoch*1000;
412                         var comment=commit.comment==null ? "" : commit.comment.join( "  " );
413                         arg.diagram.add_node( commit.id, committer_time, author_time, commit.author, comment, commit.parents, arg.repo_name );
414                 }
415         }
417 // arg:
418 //      repos: as returned by repos_decode_location
419 //      diagram: diagram
420 //      exclude_commits: [], as passed to apply_handler first arg in filter_dialog
421 //      paths: [], as passed to apply_handler first arg in filter_dialog
422 //      commits_first_loaded_handler: function( arg )
423 GitBrowser.commits_load_first=function( arg )
425         var chain=[];
426         for( var repo_name in arg.repos ) {
427                 var repo=arg.repos[repo_name];
428                 var refs=[];
429                 if( repo.all_heads ) {
430                         refs.push( "r,all" );
431                 }
432                 for( var head_i=0; head_i<repo.heads.length; ++head_i ) {
433                         refs.push( "h,"+repo.heads[head_i] );
434                 }
435                 for( var tag_i=0; tag_i<repo.tags.length; ++tag_i ) {
436                         refs.push( "t,"+repo.tags[tag_i] );
437                 }
438                 refs.sort();
439                 chain.push( { sub: "commits_from_refs", repo: repo_name, handler_arg: { diagram: arg.diagram, repo_name: repo_name },
440                                         sub_args: { ref: refs, x: arg.exclude_commits, path: arg.paths, shortcomment: [arg.shortcomment] }
441                         } );
442         }
444         GitBrowser.status_show( "loading..." );
445         GitBrowser.call_server( { handler: GitBrowser._add_refs_and_commits,
446                                         final_handler: function( arg ) { GitBrowser.status_show( "" ); arg.commits_first_loaded_handler( arg ); }, final_handler_arg: arg,
447                                         chain: chain } );
449 // arg:
450 //      diagram: diagram
451 //      exclude_commits: [], as passed to apply_handler first arg in filter_dialog
452 //      paths: [], as passed to apply_handler first arg in filter_dialog
453 //      commits_more_loaded_handler: function( arg )
454 GitBrowser.commits_load_more=function( arg )
456         var repo_map={};
457         var more_ids=arg.diagram.get_start_more_ids();
458         for( var i=0; i<more_ids.length; ++i ) {
459                 var id=more_ids[i];
460                 for( var repo_i=0; repo_i<id.repos.length; ++repo_i ) {
461                         var repo_name=id.repos[repo_i];
462                         if( repo_map[repo_name]==null ) {
463                                 repo_map[repo_name]=[[]];
464                         }
465                         var ids=repo_map[repo_name][repo_map[repo_name].length-1];
466                         if( ids.length>9 ) { // split to avoid too long urls - for now, the limit is 10 40-byte ids per url.
467                                                 // since server does not keep track which commits were already sent to which client,
468                                                 // splitting requests may cause redundant data to be transferred.
469                                 repo_map[repo_name].push( [] );
470                                 ids=repo_map[repo_name][repo_map[repo_name].length-1];
471                         }
472                         ids.push( id.id );
473                 }
474         }
475         var chain=[];
476         for( var repo_name in repo_map ) {
477                 var ids_a=repo_map[repo_name];
478                 for( var i=0; i<ids_a.length; ++i ) {
479                         var ids=ids_a[i];
480                         ids.sort();
481                         chain.push( { sub: "commits_from_ids", repo: repo_name, handler_arg: { diagram: arg.diagram, repo_name: repo_name },
482                                         sub_args: { id: ids, x: arg.exclude_commits, path: arg.paths, shortcomment: [arg.shortcomment] }
483                                 } );
484                 }
485         }
486         GitBrowser.status_show( "loading..." );
487         GitBrowser.call_server( { handler: GitBrowser._add_commits,
488                                                 final_handler: function( arg ) { GitBrowser.status_show( "" ); arg.commits_more_loaded_handler( arg ); }, final_handler_arg: arg,
489                                                 chain: chain } );
492 // glue code that appears to be common between by-date.html and by-commits.html
493 // diagram ui handler. first argument should be ui handlers map: event_name=>handler
494 GitBrowser.diagram_ui_handler=function()
496         var ui_map=arguments[0];
497         var event_name=arguments[1];
498         var args=[];
499         for( var i=2; i<arguments.length; ++i ) {
500                 args.push(arguments[i] );
501         }
502         var handler=ui_map[event_name];
503         if( handler!=null ) {
504                 handler.apply( this, args );
505         }
507 // filter
508 GitBrowser.filter_dialog_handler=function( arg, context )
510         context.exclude_commits=arg.exclude_commits;
511         context.paths=arg.paths;
512         context.diagram.clear();
513         GitBrowser.commits_load_first( context );
515 GitBrowser.filter_dialog_create=function( filter_button, context )
517         var ref_pos=Motion.get_page_coords( filter_button );
518         var x=ref_pos.x+filter_button.clientWidth;
519         var y=ref_pos.y+2+filter_button.scrollHeight;
520         GitBrowser.filter_dialog_init( { show_button: filter_button, x: x, y: y, apply_handler: GitBrowser.filter_dialog_handler, apply_handler_context: context } );
523 // arg:
524 //      repos: as as returned by repos_decode_location
525 //      diagram: GitDiagram object
526 //      title_loaded_handler
527 //      commits_first_loaded_handler
528 //      commits_more_loaded_handler
529 GitBrowser.init=function( arg )
531         GitBrowser.setup_status_error();
532         arg.load_more_button_init=function( b ) { b.href="#"; b.onclick=function() { GitBrowser.commits_load_more( arg ) }; };
533         arg.filter_button_init=GitBrowser.filter_dialog_create;
534         GitBrowser.title_init( arg );