This post is a testament to Fossil's design and the ease of reading its
implementation.  I've also added a Fossil patch at the bottom.  I humbly
hope the devs will incorporate it into the product, or tell me what is
wrong with it so I can perhaps fix it.  Otherwise, I hope the community
finds it useful or at least interesting.

Someone recommended to our team that we start using Slack (http://slack.com)
for intra-team instant messaging, file exchange, etc.  One of its nice
features is that it has automated hooks from Jira (a ticket system, as you
probably know) and Bitbucket (a Git hosting blah blah whatever, as you
probably know).  You change a ticket status or make a commit, you
immediately get a little IM to your team with a hyperlink to what
happened.  Fancy.  Slack is pretty cool in general, so it seems likely that
my team will keep using it.

I'm pushing for Fossil within my team.  The size of the Git ecosystem is a
point in Git's favor and a ding on Fossil.  Wishing to erode from this
vis-a-vis the Git->Slack integration, I asked myself, "How fast can we hook
fossil up to this bad boy?"

Answer: really fast.

It requires a little hacking of the Fossil source to expose some data to
the TH1 "Transfer" hooks.  But basically we can just use the
Admin->Transfers->Push and Admin->Transfers->Ticket TH1 hooks to get what
we want.  Slack has a nice little HTTP API for messaging, so once the data
is in TH1 you just massage it into an appropriate HTTP call.

So the idea can be easily extended to any HTTP service that accepts active
notifications.

Perhaps there is a better way to accomplish this.  But a bunch of googling
& mailing list scraping yielded only Andreas's 'fx' package, which seemed
slightly larger than what I was looking for.  Let me know if I missed
something.

Here's my first hack at the TH1 script for publishing ticket info to the
remote HTTP API:

===
tclInvoke package require http
tclInvoke package require tls
tclInvoke http::register https 443 tls::socket
set url "https://slack.com/api/chat.postMessage";

query {SELECT title, assignedTo, status, resolution, releaseGate, type,
priority, severity FROM ticket WHERE tkt_uuid=$uuid} {
  set msgText "*Title*:
<https://<anonymized>/tktview?name=$uuid|$title>\n\n*Type*:
$type\n*Priority*: $priority\n*Assigned To*: $assignedTo\n*Status*: $status"

  if {$status ne "Open"} {
    set msgText "$msgText\n*Resolution*: $resolution"
  }
  if {$releaseGate ne ""} {
    set msgText "$msgText\n*Release Gate*: $releaseGate"
  }

  set iconUrl "http://fossil-scm.org/index.html/logo";

  set quer [tclInvoke http::formatQuery token <anonymized> channel
"#tickets" username "Fossil" text $msgText icon_url $iconUrl]

  set token [tclInvoke http::geturl $url -method POST -timeout 30000 -query
$quer]

  set status [tclInvoke http::status $token]

  tclInvoke http::cleanup $token
  tclInvoke http::unregister https
}

===

Easy enough!

The issue with this is that we need to set the $uuid variable from the C
implementation prior to invoking the script.  The issue is slightly
trickier for commit notifications (the Admin->Transfers->Push case), since
we can receive multiple artifacts in one Push operation.  I opted to
provide a list of UUIDs to a single invocation of the script, rather than
one UUID for each of multiple invocations.  This allows the script more
flexibility.  (Right now the variable is called $uuid rather than e.g.
$uuidList, which is something I can fix if the devs like the general
approach.)

Critiques are certainly welcome.  Mathematicians say, "never trust any
proof written after 11pm" -- this probably should be extended to code. :-)

If the devs think the patch has merit, then I'll be happy to add comments
etc.

It was a pleasure as always to do a bit of work on a DRH project.  DRH's
code has the all-too-rare property that what you think will work, usually
_does_ work -- the first time -- even if you are brand new to the code.
This is *way* harder than it looks.

If the patch is accepted, then I think it will more easily allow future
official support of Fossil from Slack, FWIW.  See this list:
https://slack.com/integrations .  It would be nice to see some little
dino-bones on that page.

Here's the patch, which is against Fossil 1.29 [3e5ebe2b90]:

===
--- fossil-src-20140612172556.orig/src/tkt.c    2014-06-12
13:33:27.000000000 -0400
+++ fossil-src-20140612172556/src/tkt.c 2014-09-04 21:26:41.743216346 -0400
@@ -317,23 +317,24 @@
 void ticket_init(void){
   const char *zConfig;
   Th_FossilInit(TH_INIT_DEFAULT);
   zConfig = ticket_common_code();
   Th_Eval(g.interp, 0, zConfig, -1);
 }

 /*
 ** Create the TH1 interpreter and load the "change" code.
 */
-int ticket_change(void){
+int ticket_change(const char* zUuid){
   const char *zConfig;
   Th_FossilInit(TH_INIT_DEFAULT);
+  Th_SetVar(g.interp, "uuid", -1, zUuid, -1);
   zConfig = ticket_change_code();
   return Th_Eval(g.interp, 0, zConfig, -1);
 }

 /*
 ** Recreate the TICKET and TICKETCHNG tables.
 */
 void ticket_create_table(int separateConnection){
   const char *zSql;

@@ -632,21 +633,21 @@
     return TH_OK;
   }else{
     if( g.thTrace ){
       Th_Trace("submit_ticket
{\n<blockquote><pre>\n%h\n</pre></blockquote>\n"
                "}<br />\n",
          blob_str(&tktchng));
     }
     ticket_put(&tktchng, zUuid,
                (g.perm.ModTkt==0 && db_get_boolean("modreq-tkt",0)==1));
   }
-  return ticket_change();
+  return ticket_change(zUuid);
 }


 /*
 ** WEBPAGE: tktnew
 ** WEBPAGE: debug_tktnew
 **
 ** Enter a new ticket.  The tktnew_template script in the ticket
 ** configuration is used.  The /tktnew page is the official ticket
 ** entry page.  The /debug_tktnew page is used for debugging the
--- fossil-src-20140612172556.orig/src/xfer.c   2014-06-12
13:33:27.000000000 -0400
+++ fossil-src-20140612172556/src/xfer.c        2014-09-04
23:55:45.281936876 -0400
@@ -108,21 +108,25 @@
 ** The content is SIZE bytes immediately following the newline.
 ** If DELTASRC exists, then the CONTENT is a delta against the
 ** content of DELTASRC.
 **
 ** If any error occurs, write a message into pErr which has already
 ** be initialized to an empty string.
 **
 ** Any artifact successfully received by this routine is considered to
 ** be public and is therefore removed from the "private" table.
 */
-static void xfer_accept_file(Xfer *pXfer, int cloneFlag){
+static void xfer_accept_file(
+  Xfer *pXfer, int cloneFlag,
+  char** pzUuidList,
+  int* pnUuidList
+){
   int n;
   int rid;
   int srcid = 0;
   Blob content, hash;
   int isPriv;

   isPriv = pXfer->nextIsPrivate;
   pXfer->nextIsPrivate = 0;
   if( pXfer->nToken<3
    || pXfer->nToken>4
@@ -150,50 +154,64 @@
   if( cloneFlag ){
     if( pXfer->nToken==4 ){
       srcid = rid_from_uuid(&pXfer->aToken[2], 1, isPriv);
       pXfer->nDeltaRcvd++;
     }else{
       srcid = 0;
       pXfer->nFileRcvd++;
     }
     rid = content_put_ex(&content, blob_str(&pXfer->aToken[1]), srcid,
                          0, isPriv);
+    if( pzUuidList ){
+      Th_FossilInit(TH_INIT_DEFAULT);
+      Th_ListAppend(g.interp,
+          pzUuidList, pnUuidList, blob_str(&pXfer->aToken[1]), -1);
+    }
     remote_has(rid);
     blob_reset(&content);
     return;
   }
   if( pXfer->nToken==4 ){
     Blob src, next;
     srcid = rid_from_uuid(&pXfer->aToken[2], 1, isPriv);
     if( content_get(srcid, &src)==0 ){
       rid = content_put_ex(&content, blob_str(&pXfer->aToken[1]), srcid,
                            0, isPriv);
+      if( pzUuidList ){
+        Th_FossilInit(TH_INIT_DEFAULT);
+        Th_ListAppend(g.interp,
+            pzUuidList, pnUuidList, blob_str(&pXfer->aToken[1]), -1);
+      }
       pXfer->nDanglingFile++;
       db_multi_exec("DELETE FROM phantom WHERE rid=%d", rid);
       if( !isPriv ) content_make_public(rid);
       blob_reset(&src);
       blob_reset(&content);
       return;
     }
     pXfer->nDeltaRcvd++;
     blob_delta_apply(&src, &content, &next);
     blob_reset(&src);
     blob_reset(&content);
     content = next;
   }else{
     pXfer->nFileRcvd++;
   }
   sha1sum_blob(&content, &hash);
   if( !blob_eq_str(&pXfer->aToken[1], blob_str(&hash), -1) ){
     blob_appendf(&pXfer->err, "content does not match sha1 hash");
   }
   rid = content_put_ex(&content, blob_str(&hash), 0, 0, isPriv);
+  if( pzUuidList ){
+    Th_FossilInit(TH_INIT_DEFAULT);
+    Th_ListAppend(g.interp, pzUuidList, pnUuidList, blob_str(&hash), -1);
+  }
   blob_reset(&hash);
   if( rid==0 ){
     blob_appendf(&pXfer->err, "%s", g.zErrMsg);
     blob_reset(&content);
   }else{
     if( !isPriv ) content_make_public(rid);
     manifest_crosslink(rid, &content, MC_NONE);
   }
   assert( blob_is_reset(&content) );
   remote_has(rid);
@@ -215,21 +233,25 @@
 ** content of DELTASRC.
 **
 ** The original size of the UUID artifact is USIZE.
 **
 ** If any error occurs, write a message into pErr which has already
 ** be initialized to an empty string.
 **
 ** Any artifact successfully received by this routine is considered to
 ** be public and is therefore removed from the "private" table.
 */
-static void xfer_accept_compressed_file(Xfer *pXfer){
+static void xfer_accept_compressed_file(
+  Xfer *pXfer,
+  char** pzUuidList,
+  int* pnUuidList
+){
   int szC;   /* CSIZE */
   int szU;   /* USIZE */
   int rid;
   int srcid = 0;
   Blob content;
   int isPriv;

   isPriv = pXfer->nextIsPrivate;
   pXfer->nextIsPrivate = 0;
   if( pXfer->nToken<4
@@ -256,20 +278,25 @@
   }
   if( pXfer->nToken==5 ){
     srcid = rid_from_uuid(&pXfer->aToken[2], 1, isPriv);
     pXfer->nDeltaRcvd++;
   }else{
     srcid = 0;
     pXfer->nFileRcvd++;
   }
   rid = content_put_ex(&content, blob_str(&pXfer->aToken[1]), srcid,
                        szC, isPriv);
+  if( pzUuidList ){
+    Th_FossilInit(TH_INIT_DEFAULT);
+    Th_ListAppend(g.interp,
+        pzUuidList, pnUuidList, blob_str(&pXfer->aToken[1]), -1);
+  }
   remote_has(rid);
   blob_reset(&content);
 }

 /*
 ** Try to send a file as a delta against its parent.
 ** If successful, return the number of bytes in the delta.
 ** If we cannot generate an appropriate delta, then send
 ** nothing and return zero.
 **
@@ -853,24 +880,26 @@

 /*
 ** Run the specified TH1 script, if any, and returns 1 on error.
 */
 int xfer_run_script(const char *zScript, const char *zUuid){
   int rc;
   if( !zScript ) return TH_OK;
   Th_FossilInit(TH_INIT_DEFAULT);
   if( zUuid ){
     rc = Th_SetVar(g.interp, "uuid", -1, zUuid, -1);
-    if( rc!=TH_OK ){
-      fossil_error(1, "%s", Th_GetResult(g.interp, 0));
-      return rc;
-    }
+  }else{
+    rc = Th_SetVar(g.interp, "uuid", -1, "", -1);
+  }
+  if( rc!=TH_OK ){
+    fossil_error(1, "%s", Th_GetResult(g.interp, 0));
+    return rc;
   }
   rc = Th_Eval(g.interp, 0, zScript, -1);
   if( rc!=TH_OK ){
     fossil_error(1, "%s", Th_GetResult(g.interp, 0));
   }
   return rc;
 }

 /*
 ** Runs the pre-transfer TH1 script, if any, and returns its return code.
@@ -911,20 +940,22 @@
   int isPush = 0;
   int nErr = 0;
   Xfer xfer;
   int deltaFlag = 0;
   int isClone = 0;
   int nGimme = 0;
   int size;
   int recvConfig = 0;
   char *zNow;
   int rc;
+  char* zUuidList = 0;
+  int nUuidList = 0;

   if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){
      fossil_redirect_home();
   }
   g.zLogin = "anonymous";
   login_set_anon_nobody_capabilities();
   login_check_credentials();
   memset(&xfer, 0, sizeof(xfer));
   blobarray_zero(xfer.aToken, count(xfer.aToken));
   cgi_set_content_type(g.zContentType);
@@ -963,42 +994,42 @@
     **
     ** Accept a file from the client.
     */
     if( blob_eq(&xfer.aToken[0], "file") ){
       if( !isPush ){
         cgi_reset_content();
         @ error not\sauthorized\sto\swrite
         nErr++;
         break;
       }
-      xfer_accept_file(&xfer, 0);
+      xfer_accept_file(&xfer, 0, &zUuidList, &nUuidList);
       if( blob_size(&xfer.err) ){
         cgi_reset_content();
         @ error %T(blob_str(&xfer.err))
         nErr++;
         break;
       }
     }else

     /*   cfile UUID USIZE CSIZE \n CONTENT
     **   cfile UUID DELTASRC USIZE CSIZE \n CONTENT
     **
     ** Accept a file from the client.
     */
     if( blob_eq(&xfer.aToken[0], "cfile") ){
       if( !isPush ){
         cgi_reset_content();
         @ error not\sauthorized\sto\swrite
         nErr++;
         break;
       }
-      xfer_accept_compressed_file(&xfer);
+      xfer_accept_compressed_file(&xfer, &zUuidList, &nUuidList);
       if( blob_size(&xfer.err) ){
         cgi_reset_content();
         @ error %T(blob_str(&xfer.err))
         nErr++;
         break;
       }
     }else

     /*   gimme UUID
     **
@@ -1269,29 +1300,32 @@
     */
     {
       cgi_reset_content();
       @ error bad\scommand:\s%F(blob_str(&xfer.line))
     }
     blobarray_reset(xfer.aToken, xfer.nToken);
     blob_reset(&xfer.line);
   }
   if( isPush ){
     if( rc==TH_OK ){
-      rc = xfer_run_script(xfer_push_code(), 0);
+      rc = xfer_run_script(xfer_push_code(), zUuidList);
       if( rc==TH_ERROR ){
         cgi_reset_content();
         @ error push\sscript\sfailed:\s%F(g.zErrMsg)
         nErr++;
       }
     }
     request_phantoms(&xfer, 500);
   }
+  if( zUuidList ){
+    Th_Free(g.interp, zUuidList);
+  }
   if( isClone && nGimme==0 ){
     /* The initial "clone" message from client to server contains no
     ** "gimme" cards. On that initial message, send the client an "igot"
     ** card for every artifact currently in the repository.  This will
     ** cause the client to create phantoms for all artifacts, which will
     ** in turn make sure that the entire repository is sent efficiently
     ** and expeditiously.
     */
     send_all(&xfer);
     if( xfer.syncPrivate ) send_private(&xfer);
@@ -1631,31 +1665,31 @@
           fflush(stdout);
         }
       }

       /*   file UUID SIZE \n CONTENT
       **   file UUID DELTASRC SIZE \n CONTENT
       **
       ** Receive a file transmitted from the server.
       */
       if( blob_eq(&xfer.aToken[0],"file") ){
-        xfer_accept_file(&xfer, (syncFlags & SYNC_CLONE)!=0);
+        xfer_accept_file(&xfer, (syncFlags & SYNC_CLONE)!=0, 0, 0);
         nArtifactRcvd++;
       }else

       /*   cfile UUID USIZE CSIZE \n CONTENT
       **   cfile UUID DELTASRC USIZE CSIZE \n CONTENT
       **
       ** Receive a compressed file transmitted from the server.
       */
       if( blob_eq(&xfer.aToken[0],"cfile") ){
-        xfer_accept_compressed_file(&xfer);
+        xfer_accept_compressed_file(&xfer, 0, 0);
         nArtifactRcvd++;
       }else

       /*   gimme UUID
       **
       ** Server is requesting a file.  If the file is a manifest, assume
       ** that the server will also want to know all of the content files
       ** associated with the manifest and send those too.
       */
       if( blob_eq(&xfer.aToken[0], "gimme")
===

--
Eric Rubin-Smith
_______________________________________________
fossil-users mailing list
fossil-users@lists.fossil-scm.org
http://lists.fossil-scm.org:8080/cgi-bin/mailman/listinfo/fossil-users

Reply via email to