/* ui-log.c: functions for log output * * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> * * Licensed under GNU General Public License v2 * (see COPYING for full license text) */#include"cgit.h"#include"ui-log.h"#include"html.h"#include"ui-shared.h"#include"argv-array.h"staticintfiles,add_lines,rem_lines,lines_counted;/* * The list of available column colors in the commit graph. */staticconstchar*column_colors_html[]={"<span class='column1'>","<span class='column2'>","<span class='column3'>","<span class='column4'>","<span class='column5'>","<span class='column6'>","</span>",};#define COLUMN_COLORS_HTML_MAX (ARRAY_SIZE(column_colors_html) - 1)staticvoidcount_lines(char*line,intsize){if(size<=0)return;if(line[0]=='+')add_lines++;elseif(line[0]=='-')rem_lines++;}staticvoidinspect_files(structdiff_filepair*pair){unsignedlongold_size=0;unsignedlongnew_size=0;intbinary=0;files++;if(ctx.repo->enable_log_linecount)cgit_diff_files(&pair->one->oid,&pair->two->oid,&old_size,&new_size,&binary,0,ctx.qry.ignorews,count_lines);}voidshow_commit_decorations(structcommit*commit){conststructname_decoration*deco;staticcharbuf[1024];buf[sizeof(buf)-1]=0;deco=get_name_decoration(&commit->object);if(!deco)return;html("<span class='decoration'>");while(deco){structobject_idpeeled;intis_annotated=0;strncpy(buf,prettify_refname(deco->name),sizeof(buf)-1);switch(deco->type){caseDECORATION_NONE:/* If the git-core doesn't recognize it, * don't display anything. */break;caseDECORATION_REF_LOCAL:cgit_log_link(buf,NULL,"branch-deco",buf,NULL,ctx.qry.vpath,0,NULL,NULL,ctx.qry.showmsg,0);break;caseDECORATION_REF_TAG:if(!peel_ref(deco->name,&peeled))is_annotated=!oidcmp(&commit->object.oid,&peeled);cgit_tag_link(buf,NULL,is_annotated?"tag-annotated-deco":"tag-deco",buf);break;caseDECORATION_REF_REMOTE:if(!ctx.repo->enable_remote_branches)break;cgit_log_link(buf,NULL,"remote-deco",NULL,oid_to_hex(&commit->object.oid),ctx.qry.vpath,0,NULL,NULL,ctx.qry.showmsg,0);break;default:cgit_commit_link(buf,NULL,"deco",ctx.qry.head,oid_to_hex(&commit->object.oid),ctx.qry.vpath);break;}deco=deco->next;}html("</span>");}staticvoidhandle_rename(structdiff_filepair*pair){/* * After we have seen a rename, we generate links to the previous * name of the file so that commit & diff views get fed the path * that is correct for the commit they are showing, avoiding the * need to walk the entire history leading back to every commit we * show in order detect renames. */if(0!=strcmp(ctx.qry.vpath,pair->two->path)){free(ctx.qry.vpath);ctx.qry.vpath=xstrdup(pair->two->path);}inspect_files(pair);}staticintshow_commit(structcommit*commit,structrev_info*revs){structcommit_list*parents=commit->parents;structcommit*parent;intfound=0,saved_fmt;structdiff_flagssaved_flags=revs->diffopt.flags;/* Always show if we're not in "follow" mode with a single file. */if(!ctx.qry.follow)return1;/* * In "follow" mode, we don't show merges. This is consistent with * "git log --follow -- <file>". */if(parents&&parents->next)return0;/* * If this is the root commit, do what rev_info tells us. */if(!parents)returnrevs->show_root_diff;/* When we get here we have precisely one parent. */parent=parents->item;/* If we can't parse the commit, let print_commit() report an error. */if(parse_commit(parent))return1;files=0;add_lines=0;rem_lines=0;revs->diffopt.flags.recursive=1;diff_tree_oid(&parent->maybe_tree->object.oid,&commit->maybe_tree->object.oid,"",&revs->diffopt);diffcore_std(&revs->diffopt);found=!diff_queue_is_empty();saved_fmt=revs->diffopt.output_format;revs->diffopt.output_format=DIFF_FORMAT_CALLBACK;revs->diffopt.format_callback=cgit_diff_tree_cb;revs->diffopt.format_callback_data=handle_rename;diff_flush(&revs->diffopt);revs->diffopt.output_format=saved_fmt;revs->diffopt.flags=saved_flags;lines_counted=1;returnfound;}staticvoidprint_commit(structcommit*commit,structrev_info*revs){structcommitinfo*info;intcolumns=revs->graph?4:3;structstrbufgraphbuf=STRBUF_INIT;structstrbufmsgbuf=STRBUF_INIT;if(ctx.repo->enable_log_filecount)columns++;if(ctx.repo->enable_log_linecount)columns++;if(revs->graph){/* Advance graph until current commit */while(!graph_next_line(revs->graph,&graphbuf)){/* Print graph segment in otherwise empty table row */html("<tr class='nohover'><td class='commitgraph'>");html(graphbuf.buf);htmlf("</td><td colspan='%d' /></tr>\n",columns);strbuf_setlen(&graphbuf,0);}/* Current commit's graph segment is now ready in graphbuf */}info=cgit_parse_commit(commit);htmlf("<tr%s>",ctx.qry.showmsg?" class='logheader'":"");if(revs->graph){/* Print graph segment for current commit */html("<td class='commitgraph'>");html(graphbuf.buf);html("</td>");strbuf_setlen(&graphbuf,0);}else{html("<td>");cgit_print_age(info->committer_date,info->committer_tz,TM_WEEK*2);html("</td>");}htmlf("<td%s>",ctx.qry.showmsg?" class='logsubject'":"");if(ctx.qry.showmsg){/* line-wrap long commit subjects instead of truncating them */size_tsubject_len=strlen(info->subject);if(subject_len>ctx.cfg.max_msg_len&&ctx.cfg.max_msg_len>=15){/* symbol for signaling line-wrap (in PAGE_ENCODING) */constcharwrap_symbol[]={' ',0xE2,0x86,0xB5,0};inti=ctx.cfg.max_msg_len-strlen(wrap_symbol);/* Rewind i to preceding space character */while(i>0&&!isspace(info->subject[i]))--i;if(!i)/* Oops, zero spaces. Reset i */i=ctx.cfg.max_msg_len-strlen(wrap_symbol);/* add remainder starting at i to msgbuf */strbuf_add(&msgbuf,info->subject+i,subject_len-i);strbuf_trim(&msgbuf);strbuf_add(&msgbuf,"\n\n",2);/* Place wrap_symbol at position i in info->subject */strcpy(info->subject+i,wrap_symbol);}}cgit_commit_link(info->subject,NULL,NULL,ctx.qry.head,oid_to_hex(&commit->object.oid),ctx.qry.vpath);show_commit_decorations(commit);html("</td><td>");cgit_open_filter(ctx.repo->email_filter,info->author_email,"log");html_txt(info->author);cgit_close_filter(ctx.repo->email_filter);if(revs->graph){html("</td><td>");cgit_print_age(info->committer_date,info->committer_tz,TM_WEEK*2);}if(!lines_counted&&(ctx.repo->enable_log_filecount||ctx.repo->enable_log_linecount)){files=0;add_lines=0;rem_lines=0;cgit_diff_commit(commit,inspect_files,ctx.qry.vpath);}if(ctx.repo->enable_log_filecount)htmlf("</td><td>%d",files);if(ctx.repo->enable_log_linecount)htmlf("</td><td><span class='deletions'>-%d</span>/""<span class='insertions'>+%d</span>",rem_lines,add_lines);html("</td></tr>\n");if((revs->graph&&!graph_is_commit_finished(revs->graph))||ctx.qry.showmsg){/* Print a second table row */html("<tr class='nohover-highlight'>");if(ctx.qry.showmsg){/* Concatenate commit message + notes in msgbuf */if(info->msg&&*(info->msg)){strbuf_addstr(&msgbuf,info->msg);strbuf_addch(&msgbuf,'\n');}format_display_notes(&commit->object.oid,&msgbuf,PAGE_ENCODING,0);strbuf_addch(&msgbuf,'\n');strbuf_ltrim(&msgbuf);}if(revs->graph){intlines=0;/* Calculate graph padding */if(ctx.qry.showmsg){/* Count #lines in commit message + notes */constchar*p=msgbuf.buf;lines=1;while((p=strchr(p,'\n'))){p++;lines++;}}/* Print graph padding */html("<td class='commitgraph'>");while(lines>0||!graph_is_commit_finished(revs->graph)){if(graphbuf.len)html("\n");strbuf_setlen(&graphbuf,0);graph_next_line(revs->graph,&graphbuf);html(graphbuf.buf);lines--;}html("</td>\n");}elsehtml("<td/>");/* Empty 'Age' column *//* Print msgbuf into remainder of table row */htmlf("<td colspan='%d'%s>\n",columns-(revs->graph?1:0),ctx.qry.showmsg?" class='logmsg'":"");html_txt(msgbuf.buf);html("</td></tr>\n");}strbuf_release(&msgbuf);strbuf_release(&graphbuf);cgit_free_commitinfo(info);}staticconstchar*disambiguate_ref(constchar*ref,int*must_free_result){structobject_idoid;structstrbuflongref=STRBUF_INIT;strbuf_addf(&longref,"refs/heads/%s",ref);if(get_oid(longref.buf,&oid)==0){*must_free_result=1;returnstrbuf_detach(&longref,NULL);}*must_free_result=0;strbuf_release(&longref);returnref;}staticchar*next_token(char**src){char*result;if(!src||!*src)returnNULL;while(isspace(**src))(*src)++;if(!**src)returnNULL;result=*src;while(**src){if(isspace(**src)){**src='\0';(*src)++;break;}(*src)++;}returnresult;}voidcgit_print_log(constchar*tip,intofs,intcnt,char*grep,char*pattern,char*path,intpager,intcommit_graph,intcommit_sort){structrev_inforev;structcommit*commit;structargv_arrayrev_argv=ARGV_ARRAY_INIT;inti,columns=commit_graph?4:3;intmust_free_tip=0;/* rev_argv.argv[0] will be ignored by setup_revisions */argv_array_push(&rev_argv,"log_rev_setup");if(!tip)tip=ctx.qry.head;tip=disambiguate_ref(tip,&must_free_tip);argv_array_push(&rev_argv,tip);if(grep&&pattern&&*pattern){pattern=xstrdup(pattern);if(!strcmp(grep,"grep")||!strcmp(grep,"author")||!strcmp(grep,"committer")){argv_array_pushf(&rev_argv,"--%s=%s",grep,pattern);}elseif(!strcmp(grep,"range")){char*arg;/* Split the pattern at whitespace and add each token * as a revision expression. Do not accept other * rev-list options. Also, replace the previously * pushed tip (it's no longer relevant). */argv_array_pop(&rev_argv);while((arg=next_token(&pattern))){if(*arg=='-'){fprintf(stderr,"Bad range expr: %s\n",arg);break;}argv_array_push(&rev_argv,arg);}}}if(!path||!ctx.cfg.enable_follow_links){/* * If we don't have a path, "follow" is a no-op so make sure * the variable is set to false to avoid needing to check * both this and whether we have a path everywhere. */ctx.qry.follow=0;}if(commit_graph&&!ctx.qry.follow){argv_array_push(&rev_argv,"--graph");argv_array_push(&rev_argv,"--color");graph_set_column_colors(column_colors_html,COLUMN_COLORS_HTML_MAX);}if(commit_sort==1)argv_array_push(&rev_argv,"--date-order");elseif(commit_sort==2)argv_array_push(&rev_argv,"--topo-order");if(path&&ctx.qry.follow)argv_array_push(&rev_argv,"--follow");argv_array_push(&rev_argv,"--");if(path)argv_array_push(&rev_argv,path);init_revisions(&rev,NULL);rev.abbrev=DEFAULT_ABBREV;rev.commit_format=CMIT_FMT_DEFAULT;rev.verbose_header=1;rev.show_root_diff=0;rev.ignore_missing=1;rev.simplify_history=1;setup_revisions(rev_argv.argc,rev_argv.argv,&rev,NULL);load_ref_decorations(NULL,DECORATE_FULL_REFS);rev.show_decorations=1;rev.grep_filter.ignore_case=1;rev.diffopt.detect_rename=1;rev.diffopt.rename_limit=ctx.cfg.renamelimit;if(ctx.qry.ignorews)DIFF_XDL_SET(&rev.diffopt,IGNORE_WHITESPACE);compile_grep_patterns(&rev.grep_filter);prepare_revision_walk(&rev);if(pager){cgit_print_layout_start();html("<table class='list nowrap'>");}html("<tr class='nohover'>");if(commit_graph)html("<th></th>");elsehtml("<th class='left'>Age</th>");html("<th class='left'>Commit message");if(pager){html(" (");cgit_log_link(ctx.qry.showmsg?"Collapse":"Expand",NULL,NULL,ctx.qry.head,ctx.qry.sha1,ctx.qry.vpath,ctx.qry.ofs,ctx.qry.grep,ctx.qry.search,ctx.qry.showmsg?0:1,ctx.qry.follow);html(")");}html("</th><th class='left'>Author</th>");if(rev.graph)html("<th class='left'>Age</th>");if(ctx.repo->enable_log_filecount){html("<th class='left'>Files</th>");columns++;}if(ctx.repo->enable_log_linecount){html("<th class='left'>Lines</th>");columns++;}html("</tr>\n");if(ofs<0)ofs=0;for(i=0;i<ofs&&(commit=get_revision(&rev))!=NULL;/* nop */){if(show_commit(commit,&rev))i++;free_commit_buffer(commit);free_commit_list(commit->parents);commit->parents=NULL;}for(i=0;i<cnt&&(commit=get_revision(&rev))!=NULL;/* nop */){/* * In "follow" mode, we must count the files and lines the * first time we invoke diff on a given commit, and we need * to do that to see if the commit touches the path we care * about, so we do it in show_commit. Hence we must clear * lines_counted here. * * This has the side effect of avoiding running diff twice * when we are both following renames and showing file * and/or line counts. */lines_counted=0;if(show_commit(commit,&rev)){i++;print_commit(commit,&rev);}free_commit_buffer(commit);free_commit_list(commit->parents);commit->parents=NULL;}if(pager){html("</table><ul class='pager'>");if(ofs>0){html("<li>");cgit_log_link("[prev]",NULL,NULL,ctx.qry.head,ctx.qry.sha1,ctx.qry.vpath,ofs-cnt,ctx.qry.grep,ctx.qry.search,ctx.qry.showmsg,ctx.qry.follow);html("</li>");}if((commit=get_revision(&rev))!=NULL){html("<li>");cgit_log_link("[next]",NULL,NULL,ctx.qry.head,ctx.qry.sha1,ctx.qry.vpath,ofs+cnt,ctx.qry.grep,ctx.qry.search,ctx.qry.showmsg,ctx.qry.follow);html("</li>");}html("</ul>");cgit_print_layout_end();}elseif((commit=get_revision(&rev))!=NULL){htmlf("<tr class='nohover'><td colspan='%d'>",columns);cgit_log_link("[...]",NULL,NULL,ctx.qry.head,NULL,ctx.qry.vpath,0,NULL,NULL,ctx.qry.showmsg,ctx.qry.follow);html("</td></tr>\n");}/* If we allocated tip then it is safe to cast away const. */if(must_free_tip)free((char*)tip);}