/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* Copyright 2013-2021 The Libfossil Authors, see LICENSES/BSD-2-Clause.txt SPDX-License-Identifier: BSD-2-Clause-FreeBSD SPDX-FileCopyrightText: 2021 The Libfossil Authors SPDX-ArtifactOfProjectName: Libfossil SPDX-FileType: Code Heavily indebted to the Fossil SCM project (https://fossil-scm.org). */ /************************************************************************* This file implements ticket-related parts of the library. */ #include "fossil-scm/internal.h" #include <assert.h> #include <string.h> /* memcmp() */ int fsl__cx_ticket_create_table(fsl_cx * const f){ fsl_db * const db = fsl_needs_repo(f); int rc; if(!db) return FSL_RC_NOT_A_REPO; rc = fsl_cx_exec_multi(f, "DROP TABLE IF EXISTS ticket;" "DROP TABLE IF EXISTS ticketchng;" ); if(!rc){ fsl_buffer * const buf = &f->cache.fileContent; fsl_buffer_reuse(buf); rc = fsl_cx_schema_ticket(f, buf); if(!rc) rc = fsl_cx_exec_multi(f, "%b", buf); } return rc; } static int fsl__tkt_field_id(fsl_list const * jli, const char *zFieldName){ int i; fsl_card_J const * jc; for(i=0; i<(int)jli->used; ++i){ jc = (fsl_card_J const *)jli->list[i]; if( !fsl_strcmp(zFieldName, jc->field) ) return i; } return -1; } int fsl__cx_ticket_load_fields(fsl_cx * const f, bool forceReload){ fsl_list * const li = &f->ticket.customFields; if(li->used){ if(!forceReload) return 0; fsl__card_J_list_free(li, false); /* Fall through and reload ... */ }else if( !fsl_needs_repo(f) ){ return FSL_RC_NOT_A_REPO; } fsl_card_J * jc; fsl_stmt q = fsl_stmt_empty; int i; int rc = fsl_cx_prepare(f, &q, "PRAGMA table_info(ticket)"); if(!rc) while( FSL_RC_STEP_ROW==fsl_stmt_step(&q) ){ char const * zFieldName = fsl_stmt_g_text(&q, 1, NULL); if(!zFieldName){ rc = FSL_RC_OOM; break; } f->ticket.hasTicket = 1; if( 0==memcmp(zFieldName,"tkt_", 4)){ if( 0==fsl_strcmp(zFieldName,"tkt_ctime")) f->ticket.hasCTime = 1; /* These are core field names, part of every fossil ticket table. */ continue; } jc = fsl_card_J_malloc(0, zFieldName, NULL); if(!jc){ rc = FSL_RC_OOM; break; } jc->flags = FSL_CARD_J_TICKET; rc = fsl_list_append(li, jc); if(rc){ fsl_card_J_free(jc); break; } } fsl_stmt_finalize(&q); if(rc) goto end; rc = fsl_cx_prepare(f, &q, "PRAGMA table_info(ticketchng)"); if(!rc) while( FSL_RC_STEP_ROW==fsl_stmt_step(&q) ){ char const * zFieldName = fsl_stmt_g_text(&q, 1, NULL); if(!zFieldName){ rc = FSL_RC_OOM; break; } f->ticket.hasChng = 1; if( 0==memcmp(zFieldName,"tkt_", 4)){ if( 0==fsl_strcmp(zFieldName,"tkt_rid")) f->ticket.hasChngRid = 1; /* These are core field names, part of every fossil ticketchng table. */ continue; } if( (i=fsl__tkt_field_id(li, zFieldName)) >= 0){ jc = (fsl_card_J*)li->list[i]; jc->flags |= FSL_CARD_J_CHNG; continue; } jc = fsl_card_J_malloc(0, zFieldName, NULL); if(!jc){ rc = FSL_RC_OOM; break; } jc->flags = FSL_CARD_J_CHNG; rc = fsl_list_append(li, jc); if(rc){ fsl_card_J_free(jc); break; } } fsl_stmt_finalize(&q); end: if(!rc){ fsl_list_sort(li, fsl__qsort_cmp_J_cards); } return rc; } static int fsl__ticket_insert(fsl_deck * const d, fsl_id_t tktId, fsl_id_t * const tgtId){ /* Derived from fossil(1) tkt.c:ticket_insert() */; fsl_cx * const f = d->f; fsl_id_t const rid = d->rid; int rc = 0; fsl_buffer * const sql1 = fsl__cx_scratchpad(f); fsl_buffer * const sql2 = fsl__cx_scratchpad(f); fsl_buffer * const sql3 = fsl__cx_scratchpad(f); fsl_db * const db = fsl_cx_db_repo(f); fsl_list const * const cf = &f->ticket.customFields; fsl_size_t i; //char const * zMimetype = NULL; fsl_stmt q = fsl_stmt_empty; char aUsed[cf->used]; assert(rid>0 && f!=NULL && db); if(0==tktId){ rc = fsl_cx_exec_multi(f, "INSERT INTO ticket(tkt_uuid, tkt_mtime) " "VALUES(%Q, 0)", d->K); if(rc) goto end; tktId = fsl_db_last_insert_id(db); } rc = fsl_buffer_append(sql1, "UPDATE OR REPLACE ticket SET tkt_mtime=?1", -1); if(0==rc && f->ticket.hasCTime){ rc = fsl_buffer_append(sql1, ", tkt_ctime=coalesce(tkt_ctime,?1)", -1); } if(rc) goto end; memset(aUsed, 0, cf->used); for(i = 0; 0==rc && i < d->J.used; ++i){ fsl_card_J const * const dJC = (fsl_card_J*)d->J.list[i]; int const j = fsl__tkt_field_id(cf, dJC->field); if(j<0){ /* Ticket has a field which this repo does not have. Skip it. */ continue; } aUsed[j] = FSL_CARD_J_TICKET; fsl_card_J const * const rJC = (fsl_card_J*)cf->list[j]; if(rJC->flags & FSL_CARD_J_TICKET){ if(dJC->append){ rc = fsl_buffer_appendf(sql1, ", %!Q=coalesce(%!Q,'') || %Q", dJC->field, dJC->field, dJC->value); }else{ rc = fsl_buffer_appendf(sql1, ", %!Q=%Q", dJC->field, dJC->value); } if(rc) break; } if(rJC->flags & FSL_CARD_J_CHNG){ rc = fsl_buffer_appendf(sql2, ",%!Q", dJC->field); if(0==rc) rc = fsl_buffer_appendf(sql3, ",%Q", dJC->value); if(rc) break; } #if 0 if(0==fsl_strcmp(dJC->field, "mimetype")){ zMimetype = dJC->value; } #endif } if(rc) goto end; /* MISSING: a block from fossil(1) tkt.c which extracts backlinks: if( rid>0 ){ for(i=0; i<p->nField; i++){ const char *zName = p->aField[i].zName; const char *zBaseName = zName[0]=='+' ? zName+1 : zName; j = fieldId(zBaseName); if( j<0 ) continue; backlink_extract(p->aField[i].zValue, zMimetype, rid, BKLNK_TICKET, p->rDate, i==0); } } That's not critical for core ticket functionality. */ rc = fsl_buffer_appendf(sql1, " WHERE tkt_id=%" FSL_ID_T_PFMT, tktId); if(rc) goto end; rc = fsl_cx_prepare(f, &q, "%b", sql1); if(rc) goto end; rc = fsl_stmt_bind_step(&q, "f", d->D); fsl_stmt_finalize(&q); if(rc) goto end; fsl_buffer_reuse(sql1); if(f->ticket.hasChngRid || sql2->used){ bool fromTkt = false; if(f->ticket.hasChngRid){ rc = fsl_buffer_append(sql2, ",tkt_rid", -1); if(0==rc) rc = fsl_buffer_appendf(sql3, ",%" FSL_ID_T_PFMT, d->rid); if(rc) goto end; } for(i = 0; 0==rc && i < cf->used; ++i){ fsl_card_J const * const rJC = (fsl_card_J*)cf->list[i]; if(0==aUsed[i] && (rJC->flags & FSL_CARD_J_BOTH)==FSL_CARD_J_BOTH){ fromTkt = true; rc = fsl_buffer_appendf(sql2, ",%!Q", rJC->field); if(0==rc) rc = fsl_buffer_appendf(sql3, ",%!Q", rJC->field); } } if(rc) goto end; if(fromTkt){ rc = fsl_cx_prepare(f, &q, "INSERT INTO ticketchng(tkt_id,tkt_mtime%b)" "SELECT %"FSL_ID_T_PFMT",?1%b " "FROM ticket WHERE tkt_id=%"FSL_ID_T_PFMT, sql2, tktId, sql3, tktId); }else{ rc = fsl_cx_prepare(f, &q, "INSERT INTO ticketchng(tkt_id,tkt_mtime%b)" "VALUES(%"FSL_ID_T_PFMT",?1%b)", sql2, tktId, sql3); } if(0==rc) rc = fsl_stmt_bind_step(&q, "f", d->D); } end: fsl_stmt_finalize(&q); fsl__cx_scratchpad_yield(f, sql1); fsl__cx_scratchpad_yield(f, sql2); fsl__cx_scratchpad_yield(f, sql3); *tgtId = tktId; return rc; } static int fsl__ticket_timeline_entry(fsl_deck * const d, bool isNew, fsl_id_t tagId){ /* Derived from fossil(1) manifest.c:mainfest_ticket_event() */; int rc; fsl_buffer * const comment = fsl__cx_scratchpad(d->f); fsl_buffer * const brief = fsl__cx_scratchpad(d->f); char * zTitle = 0; char * zNewStatus = 0; fsl_db * const db = fsl_cx_db_repo(d->f); fsl_cx * const f = d->f; if(!f->ticket.titleColumn){ assert(!f->ticket.statusColumn); rc = fsl_db_get_text(db, &f->ticket.titleColumn, NULL, "SELECT coalesce(" "(SELECT value FROM config WHERE name='ticket-title-expr')," "'title')"); if(0==rc){ rc = fsl_db_get_text(db, &f->ticket.statusColumn, NULL, "SELECT coalesce(" "(SELECT value FROM config WHERE name='ticket-status-column')," "'status')"); } if(rc) return fsl_cx_uplift_db_error( f, db ); } rc = fsl_db_get_text(db, &zTitle, NULL, "SELECT coalesce(%!Q,'unknown') " "FROM ticket WHERE tkt_uuid=%Q", f->ticket.titleColumn, d->K); if(rc){ fsl_cx_uplift_db_error(d->f, db); goto end; } if(isNew){ rc = fsl_buffer_appendf(comment, "New ticket [%!S|%S] <i>%h</i>.", d->K, d->K, zTitle); if(0==rc){ rc = fsl_buffer_appendf(brief, "New ticket [%!S|%S].", d->K, d->K); } if(rc) goto end; }else{ // Update an existing ticket... char * zNewStatus = 0; for(fsl_size_t i = 0; i < d->J.used; ++i){ fsl_card_J const * const jc = (fsl_card_J*)d->J.list[i]; if(0==fsl_strcmp(jc->field, f->ticket.statusColumn)){ zNewStatus = jc->value; break; } } if(zNewStatus){ rc = fsl_buffer_appendf(comment, "%h ticket [%!S|%S]: <i>%h</i>", zNewStatus, d->K, d->K, zTitle); if(!rc && d->J.used>1){ rc = fsl_buffer_appendf(comment, " plus %d other change%s", (int)d->J.used-1, d->J.used==2 ? "" : "s"); } if(0==rc) rc = fsl_buffer_appendf(brief, "%h ticket [%!S|%S].", zNewStatus, d->K, d->K); if(rc) goto end; }else{ rc = fsl_db_get_text(db, &zNewStatus, NULL, "SELECT coalesce(%!Q,'unknown') " "FROM ticket WHERE tkt_uuid=%Q", f->ticket.statusColumn, d->K); if(rc){ rc = fsl_cx_uplift_db_error2(f, db, rc); goto end; } rc = fsl_buffer_appendf(comment, "Ticket [%!S|%S] <i>%h</i> " "status still %h with %d other change%s", d->K, d->K, zTitle, zNewStatus, (int)d->J.used, 1==d->J.used ? "" : "s"); fsl_free(zNewStatus); if(rc) goto end; rc = fsl_buffer_appendf(brief, "Ticket [%!S|%S]: %d change%s", d->K, d->K, (int)d->J.used, 1==d->J.used ? "" : "s"); if(rc) goto end; } } assert(0==rc); // MISSING: manifest_create_event_triggers() rc = fsl_cx_exec(d->f, "REPLACE INTO event" "(type, tagid, mtime, objid, user, comment, brief) " "VALUES('t', %"FSL_ID_T_PFMT", %"FSL_JULIAN_T_PFMT", " "%"FSL_ID_T_PFMT",%Q,%B,%B)", tagId, d->D, d->rid, d->U, comment, brief); end: fsl_free(zTitle); fsl_free(zNewStatus); fsl__cx_scratchpad_yield(d->f, comment); fsl__cx_scratchpad_yield(d->f, brief); return rc; } int fsl__ticket_rebuild(fsl_cx * const f, char const * zTktKCard){ int rc; fsl_id_t tktId; fsl_id_t tagId; fsl_db * const db = fsl_needs_repo(f); fsl_stmt q = fsl_stmt_empty; if(!db) return FSL_RC_NOT_A_REPO; assert(!f->cache.isCrosslinking); rc = fsl__cx_ticket_load_fields(f, false); if(rc) goto end; else if(!f->ticket.hasTicket) return 0; char * const zTag = fsl_mprintf("tkt-%s", zTktKCard); if(!zTag){ rc = FSL_RC_OOM; goto end; } tagId = fsl_tag_id(f, zTag, true); fsl_free(zTag); if(tagId<0){ rc = f->error.code; assert(0!=rc); goto end; } tktId = fsl_db_g_id(db, 0, "SELECT tkt_id FROM ticket " "WHERE tkt_uuid=%Q", zTktKCard); if(tktId>0){ if(f->ticket.hasChng){ rc = fsl_cx_exec(f, "DELETE FROM ticketchng " "WHERE tkt_id=%" FSL_ID_T_PFMT, tktId); } if(!rc) rc = fsl_cx_exec(f, "DELETE FROM ticket " "WHERE tkt_id=%" FSL_ID_T_PFMT, tktId); if(rc) goto end; } tktId = 0; rc = fsl_cx_prepare(f, &q, "SELECT rid FROM tagxref " "WHERE tagid=%" FSL_ID_T_PFMT " ORDER BY mtime", tagId); int counter = 0; /* Potential TODO (fossil does not do this): DELETE FROM EVENT WHERE tagid=${tagId} */ while(0==rc && FSL_RC_STEP_ROW==fsl_stmt_step(&q)){ fsl_deck deck = fsl_deck_empty; fsl_id_t const rid = fsl_stmt_g_id(&q, 0); rc = fsl_deck_load_rid(f, &deck, rid, FSL_SATYPE_TICKET); if(rc) goto outro; assert(deck.rid==rid); rc = fsl__ticket_insert(&deck, tktId, &tktId); if(0==rc){ rc = fsl__ticket_timeline_entry(&deck, 0==counter++, tagId); if(0==rc) rc = fsl__call_xlink_listeners(&deck); } outro: fsl_deck_finalize(&deck); } end: fsl_stmt_finalize(&q); return rc; }