Login
Artifact [8a831415ec]
Login

Artifact 8a831415eceb5c08691c57a8e79532d58a491b7d:


/* -*- 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;
}