Fossil SCM

fossil-scm / src / hook.c
Blame History Raw 547 lines
1
/*
2
** Copyright (c) 2020 D. Richard Hipp
3
**
4
** This program is free software; you can redistribute it and/or
5
** modify it under the terms of the Simplified BSD License (also
6
** known as the "2-Clause License" or "FreeBSD License".)
7
**
8
** This program is distributed in the hope that it will be useful,
9
** but without any warranty; without even the implied warranty of
10
** merchantability or fitness for a particular purpose.
11
**
12
** Author contact information:
13
** [email protected]
14
** http://www.hwaci.com/drh/
15
**
16
*******************************************************************************
17
**
18
** This file implements "hooks" - external programs that can be run
19
** when various events occur on a Fossil repository.
20
**
21
** Hooks are stored in the following CONFIG variables:
22
**
23
** hooks A JSON-array of JSON objects. Each object describes
24
** a single hook. Example:
25
** {
26
** "type": "after-receive", // type of hook
27
** "cmd": "command-to-run", // command to run
28
** "seq": 50 // run in this order
29
** }
30
**
31
** hook-last-rcvid The last rcvid for which post-receive hooks were
32
** run.
33
**
34
** hook-embargo Do not run hooks again before this Julian day.
35
**
36
** For "after-receive" hooks, a list of the received artifacts is sent
37
** into the command via standard input. Each line of input begins with
38
** the hash of the artifact and continues with a description of the
39
** interpretation of the artifact.
40
*/
41
#include "config.h"
42
#include "hook.h"
43
44
/*
45
** SETTING: hooks sensitive width=40 block-text
46
** The "hooks" setting contains JSON that describes all defined
47
** hooks. The value is an array of objects. Each object describes
48
** a single hook. Example:
49
**
50
**
51
** {
52
** "type": "after-receive", // type of hook
53
** "cmd": "command-to-run", // command to run
54
** "seq": 50 // run in this order
55
** }
56
*/
57
/*
58
** List of valid hook types:
59
*/
60
static const char *azType[] = {
61
"after-receive",
62
"before-commit",
63
"disabled",
64
};
65
66
/*
67
** Return true if zType is a valid hook type.
68
*/
69
static int is_valid_hook_type(const char *zType){
70
int i;
71
for(i=0; i<count(azType); i++){
72
if( strcmp(azType[i],zType)==0 ) return 1;
73
}
74
return 0;
75
}
76
77
/*
78
** Throw an error if zType is not a valid hook type
79
*/
80
static void validate_type(const char *zType){
81
int i;
82
char *zMsg;
83
if( is_valid_hook_type(zType) ) return;
84
zMsg = mprintf("\"%s\" is not a valid hook type - should be one of:", zType);
85
for(i=0; i<count(azType); i++){
86
zMsg = mprintf("%z %s", zMsg, azType[i]);
87
}
88
fossil_fatal("%s", zMsg);
89
}
90
91
/*
92
** Translate a hook command string into its executable format by
93
** converting every %-substitutions as follows:
94
**
95
** %F -> Name of the fossil executable
96
** %R -> Name of the repository
97
** %A -> Auxiliary information filename (might be empty string)
98
**
99
** The returned string is obtained from fossil_malloc() and should
100
** be freed by the caller.
101
*/
102
static char *hook_subst(
103
const char *zCmd,
104
const char *zAuxFilename /* Name of auxiliary information file */
105
){
106
Blob r;
107
int i;
108
blob_init(&r, 0, 0);
109
if( zCmd==0 ) return 0;
110
while( zCmd[0] ){
111
for(i=0; zCmd[i] && zCmd[i]!='%'; i++){}
112
blob_append(&r, zCmd, i);
113
if( zCmd[i]==0 ) break;
114
if( zCmd[i+1]=='F' ){
115
blob_append(&r, g.nameOfExe, -1);
116
zCmd += i+2;
117
}else if( zCmd[i+1]=='R' ){
118
blob_append(&r, g.zRepositoryName, -1);
119
zCmd += i+2;
120
}else if( zCmd[i+1]=='A' ){
121
if( zAuxFilename ) blob_append(&r, zAuxFilename, -1);
122
zCmd += i+2;
123
}else{
124
blob_append(&r, zCmd+i, 1);
125
zCmd += i+1;
126
}
127
}
128
blob_str(&r);
129
return r.aData;
130
}
131
132
/*
133
** Record the fact that new artifacts are expected within N seconds
134
** (N is normally a small number) and so post-receive hooks should
135
** probably be deferred until after the new artifacts arrive.
136
**
137
** If N==0, then there is no expectation of new artifacts arriving
138
** soon and so post-receive hooks can be run without delay.
139
*/
140
void hook_expecting_more_artifacts(int N){
141
if( !db_is_writeable("repository") ){
142
/* No-op */
143
}else if( N>0 ){
144
db_unprotect(PROTECT_CONFIG);
145
db_multi_exec(
146
"REPLACE INTO config(name,value,mtime)"
147
"VALUES('hook-embargo',now()+%d,now())",
148
N
149
);
150
db_protect_pop();
151
}else{
152
db_unset("hook-embargo",0);
153
}
154
}
155
156
157
/*
158
** Fill the Blob pOut with text that describes all artifacts
159
** received after zBaseRcvid up to and including zNewRcvid.
160
** Except, never include more than one days worth of changes.
161
**
162
** If zBaseRcvid is NULL, then use the "hook-last-rcvid" setting.
163
** If zNewRcvid is NULL, use the last available rcvid.
164
*/
165
void hook_changes(Blob *pOut, const char *zBaseRcvid, const char *zNewRcvid){
166
char *zWhere;
167
Stmt q;
168
if( zBaseRcvid==0 ){
169
zBaseRcvid = db_get("hook-last-rcvid","0");
170
}
171
if( zNewRcvid==0 ){
172
zNewRcvid = db_text("0","SELECT max(rcvid) FROM rcvfrom");
173
}
174
175
/* Adjust the baseline rcvid to omit change that are more than
176
** 24 hours older than the most recent change.
177
*/
178
zBaseRcvid = db_text(0,
179
"SELECT min(rcvid) FROM rcvfrom"
180
" WHERE rcvid>=%d"
181
" AND mtime>=(SELECT mtime FROM rcvfrom WHERE rcvid=%d)-1.0",
182
atoi(zBaseRcvid), atoi(zNewRcvid)
183
);
184
185
zWhere = mprintf("IN (SELECT rid FROM blob WHERE rcvid>%d AND rcvid<=%d)",
186
atoi(zBaseRcvid), atoi(zNewRcvid));
187
describe_artifacts(zWhere);
188
fossil_free(zWhere);
189
db_prepare(&q, "SELECT uuid, summary FROM description");
190
while( db_step(&q)==SQLITE_ROW ){
191
blob_appendf(pOut, "%s %s\n", db_column_text(&q,0), db_column_text(&q,1));
192
}
193
db_finalize(&q);
194
}
195
196
/*
197
** COMMAND: hook*
198
**
199
** Usage: %fossil hook COMMAND ...
200
**
201
** Commands include:
202
**
203
** > fossil hook add --command COMMAND --type TYPE --sequence NUMBER
204
**
205
** Create a new hook. The --command and --type arguments are
206
** required. --sequence is optional.
207
**
208
** > fossil hook delete ID ...
209
**
210
** Delete one or more hooks by their IDs. ID can be "all"
211
** to delete all hooks. Caution: There is no "undo" for
212
** this operation. Deleted hooks are permanently lost.
213
**
214
** > fossil hook edit --command COMMAND --type TYPE --sequence NUMBER ID ...
215
**
216
** Make changes to one or more existing hooks. The ID argument
217
** is either a hook-id, or a list of hook-ids, or the keyword
218
** "all". For example, to disable hook number 2, use:
219
**
220
** fossil hook edit --type disabled 2
221
**
222
** > fossil hook list
223
**
224
** Show all current hooks
225
**
226
** > fossil hook status
227
**
228
** Print the values of CONFIG table entries that are relevant to
229
** hook processing. Used for debugging.
230
**
231
** > fossil hook test [OPTIONS] ID
232
**
233
** Run the hook script given by ID for testing purposes.
234
** Options:
235
**
236
** --dry-run Print the script on stdout rather than run it
237
** --base-rcvid N Pretend that the hook-last-rcvid value is N
238
** --new-rcvid M Pretend that the last rcvid value is M
239
** --aux-file NAME NAME is substituted for %A in the script
240
**
241
** The --base-rcvid and --new-rcvid options are silently ignored if
242
** the hook type is not "after-receive". The default values for
243
** --base-rcvid and --new-rcvid cause the last receive to be processed.
244
*/
245
void hook_cmd(void){
246
const char *zCmd;
247
int nCmd;
248
db_find_and_open_repository(0, 0);
249
if( g.argc<3 ){
250
usage("SUBCOMMAND ...");
251
}
252
zCmd = g.argv[2];
253
nCmd = (int)strlen(zCmd);
254
if( strncmp(zCmd, "add", nCmd)==0 ){
255
const char *zCmd = find_option("command",0,1);
256
const char *zType = find_option("type",0,1);
257
const char *zSeq = find_option("sequence",0,1);
258
int nSeq;
259
verify_all_options();
260
if( zCmd==0 || zType==0 ){
261
fossil_fatal("the --command and --type options are required");
262
}
263
validate_type(zType);
264
nSeq = zSeq ? atoi(zSeq) : 10;
265
db_begin_write();
266
db_unprotect(PROTECT_CONFIG);
267
db_multi_exec(
268
"INSERT OR IGNORE INTO config(name,value) VALUES('hooks','[]');\n"
269
"UPDATE config"
270
" SET value=json_insert("
271
" CASE WHEN json_valid(value) THEN value ELSE '[]' END,'$[#]',"
272
" json_object('cmd',%Q,'type',%Q,'seq',%d)),"
273
" mtime=now()"
274
" WHERE name='hooks';",
275
zCmd, zType, nSeq
276
);
277
db_protect_pop();
278
db_commit_transaction();
279
}else
280
if( strncmp(zCmd, "edit", nCmd)==0 ){
281
const char *zCmd = find_option("command",0,1);
282
const char *zType = find_option("type",0,1);
283
const char *zSeq = find_option("sequence",0,1);
284
int nSeq;
285
int i;
286
verify_all_options();
287
if( zCmd==0 && zType==0 && zSeq==0 ){
288
fossil_fatal("at least one of --command, --type, or --sequence"
289
" is required");
290
}
291
if( zType ) validate_type(zType);
292
nSeq = zSeq ? atoi(zSeq) : 10;
293
if( g.argc<4 ) usage("delete ID ...");
294
db_begin_write();
295
for(i=3; i<g.argc; i++){
296
Blob sql;
297
int id;
298
if( sqlite3_strglob("*[^0-9]*", g.argv[i])==0 ){
299
fossil_fatal("not a valid ID: \"%s\"", g.argv[i]);
300
}
301
id = atoi(g.argv[i]);
302
blob_init(&sql, 0, 0);
303
blob_append_sql(&sql, "UPDATE config SET mtime=now(), value="
304
"json_replace(CASE WHEN json_valid(value) THEN value ELSE '[]' END");
305
if( zCmd ){
306
blob_append_sql(&sql, ",'$[%d].cmd',%Q", id, zCmd);
307
}
308
if( zType ){
309
blob_append_sql(&sql, ",'$[%d].type',%Q", id, zType);
310
}
311
if( zSeq ){
312
blob_append_sql(&sql, ",'$[%d].seq',%d", id, nSeq);
313
}
314
blob_append_sql(&sql,") WHERE name='hooks';");
315
db_unprotect(PROTECT_CONFIG);
316
db_multi_exec("%s", blob_sql_text(&sql));
317
db_protect_pop();
318
blob_reset(&sql);
319
}
320
db_commit_transaction();
321
}else
322
if( strncmp(zCmd, "delete", nCmd)==0 ){
323
int i;
324
verify_all_options();
325
if( g.argc<4 ) usage("delete ID ...");
326
db_begin_write();
327
db_unprotect(PROTECT_CONFIG);
328
db_multi_exec(
329
"INSERT OR IGNORE INTO config(name,value) VALUES('hooks','[]');\n"
330
);
331
for(i=3; i<g.argc; i++){
332
const char *zId = g.argv[i];
333
if( strcmp(zId,"all")==0 ){
334
db_unprotect(PROTECT_ALL);
335
db_set("hooks","[]", 0);
336
db_protect_pop();
337
break;
338
}
339
if( sqlite3_strglob("*[^0-9]*", g.argv[i])==0 ){
340
fossil_fatal("not a valid ID: \"%s\"", g.argv[i]);
341
}
342
db_multi_exec(
343
"UPDATE config"
344
" SET value=json_remove("
345
" CASE WHEN json_valid(value) THEN value ELSE '[]' END,'$[%d]'),"
346
" mtime=now()"
347
" WHERE name='hooks';",
348
atoi(zId)
349
);
350
}
351
db_protect_pop();
352
db_commit_transaction();
353
}else
354
if( strncmp(zCmd, "list", nCmd)==0 ){
355
Stmt q;
356
int n = 0;
357
verify_all_options();
358
db_prepare(&q,
359
"SELECT jx.key,"
360
" jx.value->>'seq',"
361
" jx.value->>'cmd',"
362
" jx.value->>'type'"
363
" FROM config, json_each(config.value) AS jx"
364
" WHERE config.name='hooks' AND json_valid(config.value)"
365
);
366
while( db_step(&q)==SQLITE_ROW ){
367
if( n++ ) fossil_print("\n");
368
fossil_print("%3d: type = %s\n",
369
db_column_int(&q,0), db_column_text(&q,3));
370
fossil_print(" command = %s\n", db_column_text(&q,2));
371
fossil_print(" sequence = %d\n", db_column_int(&q,1));
372
}
373
db_finalize(&q);
374
}else
375
if( strncmp(zCmd, "status", nCmd)==0 ){
376
Stmt q;
377
db_prepare(&q,
378
"SELECT name, quote(value) FROM config WHERE name IN"
379
"('hooks','hook-embargo','hook-last-rcvid') ORDER BY name"
380
);
381
while( db_step(&q)==SQLITE_ROW ){
382
fossil_print("%s: %s\n", db_column_text(&q,0), db_column_text(&q,1));
383
}
384
db_finalize(&q);
385
}else
386
if( strncmp(zCmd, "test", nCmd)==0 ){
387
Stmt q;
388
int id;
389
int bDryRun = find_option("dry-run", "n", 0)!=0;
390
const char *zOrigRcvid = find_option("base-rcvid",0,1);
391
const char *zNewRcvid = find_option("new-rcvid",0,1);
392
const char *zAuxFilename = find_option("aux-file",0,1);
393
verify_all_options();
394
if( g.argc<4 ) usage("test ID");
395
id = atoi(g.argv[3]);
396
if( zOrigRcvid==0 ){
397
zOrigRcvid = db_text(0, "SELECT max(rcvid)-1 FROM rcvfrom");
398
}
399
db_prepare(&q,
400
"SELECT value->>'$[%d].cmd', value->>'$[%d].type'=='after-receive'"
401
" FROM config"
402
" WHERE name='hooks' AND json_valid(value)",
403
id, id
404
);
405
while( db_step(&q)==SQLITE_ROW ){
406
const char *zCmd = db_column_text(&q,0);
407
char *zCmd2 = hook_subst(zCmd, zAuxFilename);
408
int needOut = db_column_int(&q,1);
409
Blob out;
410
if( zCmd2==0 ) continue;
411
blob_init(&out,0,0);
412
if( needOut ) hook_changes(&out, zOrigRcvid, zNewRcvid);
413
if( bDryRun ){
414
fossil_print("%s\n", zCmd2);
415
if( needOut ){
416
fossil_print("%s", blob_str(&out));
417
}
418
}else if( needOut ){
419
int fdFromChild;
420
FILE *toChild;
421
int pidChild;
422
if( popen2(zCmd2, &fdFromChild, &toChild, &pidChild, 0)==0 ){
423
if( toChild ){
424
fwrite(blob_buffer(&out),1,blob_size(&out),toChild);
425
}
426
pclose2(fdFromChild, toChild, pidChild);
427
}
428
}else{
429
fossil_system(zCmd2);
430
}
431
fossil_free(zCmd2);
432
blob_reset(&out);
433
}
434
db_finalize(&q);
435
}else
436
{
437
fossil_fatal("unknown command \"%s\" - should be one of: "
438
"add delete edit list test", zCmd);
439
}
440
}
441
442
/*
443
** The backoffice calls this routine to run the after-receive hooks.
444
*/
445
int hook_backoffice(void){
446
Stmt q;
447
const char *zLastRcvid = 0;
448
char *zNewRcvid = 0;
449
Blob chng;
450
int cnt = 0;
451
db_begin_write();
452
if( !db_exists("SELECT 1 FROM config WHERE name='hooks'") ){
453
goto hook_backoffice_done; /* No hooks */
454
}
455
if( db_int(0, "SELECT now()<value+0 FROM config"
456
" WHERE name='hook-embargo'") ){
457
goto hook_backoffice_done; /* Within the embargo window */
458
}
459
zLastRcvid = db_get("hook-last-rcvid","0");
460
zNewRcvid = db_text("0","SELECT max(rcvid) FROM rcvfrom");
461
if( atoi(zLastRcvid)>=atoi(zNewRcvid) ){
462
goto hook_backoffice_done; /* no new content */
463
}
464
blob_init(&chng, 0, 0);
465
db_prepare(&q,
466
"SELECT jx.value->>'cmd'"
467
" FROM config, json_each(config.value) AS jx"
468
" WHERE config.name='hooks' AND json_valid(config.value)"
469
" AND jx.value->>'type'='after-receive'"
470
" ORDER BY jx.value->>'seq';"
471
);
472
while( db_step(&q)==SQLITE_ROW ){
473
char *zCmd;
474
int fdFromChild;
475
FILE *toChild;
476
int childPid;
477
if( cnt==0 ){
478
hook_changes(&chng, zLastRcvid, 0);
479
}
480
zCmd = hook_subst(db_column_text(&q,0), 0);
481
if( popen2(zCmd, &fdFromChild, &toChild, &childPid, 0)==0 ){
482
if( toChild ){
483
fwrite(blob_buffer(&chng),1,blob_size(&chng),toChild);
484
}
485
pclose2(fdFromChild, toChild, childPid);
486
}
487
fossil_free(zCmd);
488
cnt++;
489
}
490
db_finalize(&q);
491
db_set("hook-last-rcvid", zNewRcvid, 0);
492
blob_reset(&chng);
493
hook_backoffice_done:
494
db_commit_transaction();
495
return cnt;
496
}
497
498
/*
499
** Return true if one or more hooks of type zType exit.
500
*/
501
int hook_exists(const char *zType){
502
return db_exists(
503
"SELECT 1"
504
" FROM config, json_each(config.value) AS jx"
505
" WHERE config.name='hooks' AND json_valid(config.value)"
506
" AND jx.value->>'type'=%Q;",
507
zType
508
);
509
}
510
511
/*
512
** Run all hooks of type zType. Use zAuxFile as the auxiliary information
513
** file.
514
**
515
** If any hook returns non-zero, then stop running and return non-zero.
516
** Return zero only if all hooks return zero.
517
*/
518
int hook_run(const char *zType, const char *zAuxFile, int traceFlag){
519
Stmt q;
520
int rc = 0;
521
if( !db_exists("SELECT 1 FROM config WHERE name='hooks'") ){
522
return 0;
523
}
524
db_prepare(&q,
525
"SELECT jx.value->>'cmd' "
526
" FROM config, json_each(config.value) AS jx"
527
" WHERE config.name='hooks' AND json_valid(config.value)"
528
" AND jx.value->>'type'==%Q"
529
" ORDER BY jx.value->'seq';",
530
zType
531
);
532
while( db_step(&q)==SQLITE_ROW ){
533
char *zCmd;
534
zCmd = hook_subst(db_column_text(&q,0), zAuxFile);
535
if( traceFlag ){
536
fossil_print("%s hook: %s\n", zType, zCmd);
537
}
538
rc = fossil_system(zCmd);
539
fossil_free(zCmd);
540
if( rc ){
541
break;
542
}
543
}
544
db_finalize(&q);
545
return rc;
546
}
547

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button