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