//Kanban boards #define _GNU_SOURCE 2 #include #include #include #include //#define DBLINE // #define DBLINE enum state_t {TODO =1, DOING, DONE,WONT,BACKLOG}; char *state_strings[] = { "INVALID", "TODO","DOING","DONE","WONT","BACKLOG"}; enum type_t {TICKET=1, COMMENT,EPIC}; char *type_strings[] = { "INVALID", "TICKET","EPIC"}; enum swimlane_t {NONE=0, TYPE, WEB}; #define MAXCOMMENTS 20 #define MAXSUPS 40 #define MAX_TICKETS 10000 typedef struct { char *name; char *desc; char *closemesg; char *assignee; char *reporter; int type; int state; int points; int priority; char *parent; time_t otime; time_t dtime; time_t ctime; time_t etime; int ephemeral; char *comments[MAXCOMMENTS]; char *sups[MAXSUPS]; int supcount; int commentcount; } ticket; ticket tickets[MAX_TICKETS]; int lastticket=-1; int defaultstate=TODO; int multiusermode=1; int fortnight=86400*14; char *dbpath; char *adbpath; char *backname=".~TODO"; char *weburl=""; char *upcmd=""; char *cssurl=""; int defaultpoints=3; int savedb=0; int indexforname(char *name){ for(int i=0;i<=lastticket;i++)if(!strcmp(name,tickets[i].name))return i; return -1; } void error(char *m,int n){ printf("%s,%d\n",m,n); exit(1); } void check(int expression, char *m) {if(!expression){error(m,expression);}} void invalidkey(int lnum,char *name,char *key,char *val){ printf("#Incompatible Datum at line number:%d will be dropped.\n",lnum); if(!strcmp(name,"global")) printf("%s=%s\n#Hint:open a [ticket] first.",key,val); else printf("[%s]\n%s=%s\n",name,key,val); } void updatesetting(int lnum,char *l){ if(index(l,'=')==NULL) return; char *val=strdup(index(l,'=')+1); val[strlen(val)-1]='\0'; *index(l,'=')='\0'; if(!strcasecmp(l,"backup"))backname=val; else if(!strcasecmp(l,"upcmd"))upcmd=val; else if(!strcasecmp(l,"weburl"))weburl=val; else if(!strcasecmp(l,"cssurl"))cssurl=val; else {//free val if(!strcasecmp(l,"fortnight"))fortnight=atoi(val); else if(!strcasecmp(l,"defaultstate"))defaultstate=atoi(val); else if(!strcasecmp(l,"multiusermode"))multiusermode=atoi(val); else if(!strcasecmp(l,"defaultpoints"))defaultpoints=atoi(val); else invalidkey(lnum,"global",l,val); free(val); } } void updatefield(int lnum,char *l,int t){ if(index(l,'=')==NULL) return; char *val=strdup(index(l,'=')+1);//TODO: Handle comments val[strlen(val)-1]='\0'; *index(l,'=')='\0'; if(!strcmp(l,"desc"))tickets[t].desc=val; else if(!strcasecmp(l,"assignee"))tickets[t].assignee=val; else if(!strcasecmp(l,"reporter"))tickets[t].reporter=val; else if(!strcasecmp(l,"parent"))tickets[t].parent=val; else if(!strcasecmp(l,"closemesg"))tickets[t].closemesg=val; else if(strcasestr(l,"sup")==l){ if(tickets[t].supcount=0){ printf("#Note: duplicate ticket on line %d. Updating ticket. New data will overwrite old.\n",lcount); curticket=indexforname(curline+1); }else{ lastticket++; memset(tickets+lastticket,0,sizeof(ticket)); tickets[lastticket].name=strdup(curline+1); curticket=lastticket; } }else if(curticket>=0){ updatefield(lcount,curline,curticket); }else updatesetting(lcount,curline); } } void tform(FILE *f, char *name,char *url) { fprintf(f,"
\n"); fprintf(f,"\n",name); } void writekey(FILE *f, char *name, char *key,char *value, int format) { if(format==WEB) { tform(f,name,weburl); fprintf(f,"%s: \n",key,key,value); fprintf(f,"
"); }else { fprintf(f,"%s=%s\n",key,value); } } void writelong(FILE *f, char *name, char *key,long int value, int format) { if(format==WEB) { tform(f,name,weburl); fprintf(f,"%s: \n",key,key,value); fprintf(f,""); }else { fprintf(f,"%s=%ld\n",key,value); } } void writeoutticket(FILE *f,ticket t,int format){ if(format==WEB) fprintf(f,"

%s

\n",t.name); else fprintf(f,"[%s]\n",t.name); writekey(f,t.name,"desc", t.desc, format); if(t.assignee!=NULL) writekey(f,t.name,"assignee",t.assignee,format); if(t.reporter!=NULL) writekey(f,t.name,"reporter",t.reporter,format); writekey(f,t.name,"state",state_strings[t.state],format); writekey(f,t.name,"type",type_strings[t.type],format); writelong(f,t.name,"points",t.points,format); if(t.otime) writelong(f,t.name,"otime",t.otime,format); if(t.dtime) writelong(f,t.name,"dtime",t.dtime,format); if(t.ctime) writelong(f,t.name,"ctime",t.ctime,format); if(t.etime) writelong(f,t.name,"etime",t.etime,format); for(int i=0;i0||!strcmp(name,"global")){ if(state==0) {printf("name already in use.\n");exit(-1);} else { tickets[indexforname(name)].state=state; return; } } if(state==0) state=defaultstate; if(argc<2){ printf("The description message is a human readable description of the work to be done, you might include the path to a wiki entry if it is complex\ndesc: "); fgets(desc,500,stdin); } lastticket++; memset(tickets+lastticket,0,sizeof(ticket)); tickets[lastticket].name=strdup(name); tickets[lastticket].desc=strdup(desc); tickets[lastticket].reporter=strdup(getenv("USER")); tickets[lastticket].otime=time(NULL); tickets[lastticket].state=state; tickets[lastticket].type=TICKET; tickets[lastticket].points=defaultpoints; } void delticket(int t) { check(t<=lastticket,"Ticket index is beyond end of ticket list"); check(t>=0,"Ticket index is before beginning of ticket list"); for(;t0){ tickets[t].state=DOING; tickets[t].dtime=time(NULL); } else printf("No such ticket.\n"); } void closeticket(int argc, char **argv){ if(argc<1){printf("USAGE: close ticket\n Move ticket to \"Done\" column.\n"); exit(-1);} int t=indexforname(argv[0]); if(t>=0){ tickets[t].state=DONE; tickets[t].ctime=time(NULL); } else printf("No such ticket.\n"); } void reportswimlane(ticket t, int swimlane){ if(t.state==BACKLOG || t.state==WONT || t.state==0) return; if(swimlane==WEB) printf(""); for(int i=TODO;i"); } if(swimlane == WEB) printf(""); if(multiusermode) printf("[%s @ %s : %d]\n",t.name,t.assignee,t.points); if(!multiusermode) printf("[%s : %d]",t.name,t.points); if(swimlane == WEB) { printf("
"); if(strlen(weburl)){ tform(stdout,t.name,weburl); if(t.state>TODO) printf("",state_strings[t.state+1]); if(t.state->",state_strings[t.state-1]); printf("
\n"); printf("Details",weburl,t.name); } } if(t.supcount && t.state != DONE){ char *status=t.sups[t.supcount-1]; if(status!=NULL&&strlen(status)>0) printf(" (%s)", status); } if(swimlane == WEB) printf("\n"); else printf("\n"); } void reportone(ticket t){ if(t.state==BACKLOG || t.state==WONT || t.state==0) return; printf("%s\t%d\t%ld\t%ld\n",t.assignee,t.points,t.ctime-t.otime,t.ctime-t.dtime); } void webheader(int board) { printf("Kanban Board\n"); if(strlen(cssurl)) printf("",cssurl); else{ printf("\n"); } if(board){ printf("

Kanban Board


\n"); printf("\n"); }else{ printf("\n"); } } void webfooter(int board) { if(board) printf("
TODODOINGDONE
\n"); else printf("\n"); } void report(int argc, char **argv,int swimlane){ if(swimlane == TYPE ) printf("TODO\t\t\t\tDOING\t\t\t\tDONE\n"); else if(swimlane == WEB) webheader(1); else printf("ASIGNEE\tPOINTS\tTIMEOPEN\tTIMEDOING\n"); for(int i=0;i<=lastticket;i++){ if(swimlane == TYPE || swimlane == WEB) reportswimlane(tickets[i],swimlane); else reportone(tickets[i]); } if(swimlane == WEB) webfooter(1); } void showticket(int argc, char **argv, int format){ if(argc<1){printf("USAGE: show ticket\n Export ticket to stdout.\n"); exit(-1);} int t=indexforname(argv[0]); if(t>=0){ writeoutticket(stdout,tickets[t],format); } else printf("No such ticket.\n"); } void webdetail(ticket t){ webheader(0); showticket(1,&t.name,WEB); webfooter(0); } void stats(int argc, char **argv){ //TODO: allow statistics to be reported over sliding time window. time_t t=time(NULL); long long catimetoclose=0; long long catimedoing=0; int totalcount=0; int opencount=0; int doingcount=0; int donecount=0; long long caage=0;//Average age of unclosed tickets const long long timediv=60*60;//an hour //TODO: weight by points. for(int i=0;i<=lastticket;i++){ if(tickets[i].otime==0) continue; //tickets with otime of zero are ignored for statistics long long timetoclose=tickets[i].ctime-tickets[i].otime; long long timetodoing=tickets[i].dtime-tickets[i].otime; long long age=t-tickets[i].otime; if(tickets[i].state==DONE){ catimetoclose=(timetoclose + (donecount*catimetoclose))/(donecount+1); donecount++; }else { caage=(age +(opencount*caage))/(opencount+1); opencount++; } if(tickets[i].state==DOING){ catimedoing=(timetodoing + (doingcount*catimedoing))/(doingcount+1); doingcount++; } totalcount++; } if(!(argc>=1 && !strcmp(argv[0],"noheader"))) printf("toclose\ttodoing\tavg age\topen\tdoing\tdone\ttotal\t\t Time in hours.\n"); printf("%lld\t%lld\t%lld\t%d\t%d\t%d\t%d\n",catimetoclose/timediv,catimedoing/timediv,caage/timediv,opencount,doingcount,donecount,totalcount); } void comment(int argc,char **argv){ if(argc<2){printf("USAGE: comment ticket \"comment\"\n add a short comment to a ticket.\n"); exit(-1);} int t=indexforname(argv[0]); if(t>=0){ if( tickets[t].commentcount=0){ if( tickets[t].supcount=0){ FILE *f=fopen(adbpath,"a"); if(f != NULL) writeoutticket(f,tickets[t],NONE); if(f != NULL && !fclose(f)){ delticket(t); } else { printf("Failed to archive ticket, it has not been deleted from %s, please check permissions.\n",dbpath); } } else {printf("No such ticket.\n");exit(-1);} } void wontticket(int argc,char **argv){ if(argc<1){printf("USAGE: wont ticket \n Mark ticket as wontdo and move ticket to %s\n",adbpath); exit(-1);} int t=indexforname(argv[0]); tickets[t].state=WONT; tickets[t].ctime=time(NULL); if(t>=0){ FILE *f=fopen(adbpath,"a"); if(f != NULL) writeoutticket(f,tickets[t],NONE); if(f != NULL && !fclose(f)){ delticket(t); } else { printf("Failed to archive ticket, it has not been deleted from %s, please check permissions.\n",dbpath); } } else {printf("No such ticket.\n");exit(-1);} } void todo(int argc,char **argv){ if(argc<1) {usage(); exit(1);} if(!strcmp(argv[0],"workflow")){workflow();exit(0);} if(!strcmp(argv[0],"check")){exit(0);} if(!strcmp(argv[0],"stats")){stats(argc-1,argv+1);exit(0);} else if(!strcmp(argv[0],"normalize")){savedb=1;} else if(!strcmp(argv[0],"create")){newticket(argc-1,argv+1,0);savedb=1;} else if(!strcmp(argv[0],"todo")){newticket(argc-1,argv+1,TODO);savedb=1;} else if(!strcmp(argv[0],"backlog")){newticket(argc-1,argv+1,BACKLOG);savedb=1;} else if(!strcmp(argv[0],"assign")){assign(argc-1,argv+1);savedb=1;} else if(!strcmp(argv[0],"do")){doticket(argc-1,argv+1);savedb=1;} else if(!strcmp(argv[0],"close")){closeticket(argc-1,argv+1);savedb=1;} else if(!strcmp(argv[0],"wont")){wontticket(argc-1,argv+1);savedb=1;} else if(!strcmp(argv[0],"import")){readintickets(stdin);savedb=1;} else if(!strcmp(argv[0],"show")){showticket(argc-1,argv+1,TYPE);} else if(!strcmp(argv[0],"webshow")){showticket(argc-1,argv+1,WEB);} else if(!strcmp(argv[0],"kanban")){report(argc-1,argv+1,TYPE);} else if(!strcmp(argv[0],"webkanban")){report(argc-1,argv+1,WEB);} else if(!strcmp(argv[0],"report")){report(argc-1,argv+1,NONE);} else if(!strcmp(argv[0],"comment")){comment(argc-1,argv+1);savedb=1;} else if(!strcmp(argv[0],"sup")){sup(argc-1,argv+1);savedb=1;} else if(!strcmp(argv[0],"archive")){archive(argc-1,argv+1);savedb=1;} else {usage();exit(1);} } int main(int argc, char **argv){ dbpath="./todo.ini"; adbpath="./todo.old.ini"; if(getenv("TTDB")!=NULL) dbpath=strdup(getenv("TTDB")); if(getenv("TTADB")!=NULL) adbpath=strdup(getenv("TTADB")); FILE *f=fopen(dbpath,"r"); if(f!=NULL){ readintickets(f); todo(argc-1,argv+1); fclose(f); if(savedb){ rename(dbpath,backname); FILE *f=fopen(dbpath,"w"); if(f!=NULL){ writeoutdb(f); }else{printf("Could not open database for writting\n");exit(-3);} } } else { printf("Please create an empty file at %s. This will be used as the ticket database.\n",dbpath); } return 0; }