Fossil SCM
Add support for before-commit hooks.
Commit
4a9c6c64ee77c6de7952de2be0e104166afb8be462da472e229b9083e682f9e4
Parent
3d191798a62507e…
2 files changed
+76
-3
+52
-6
+76
-3
| --- src/checkin.c | ||
| +++ src/checkin.c | ||
| @@ -1323,10 +1323,66 @@ | ||
| 1323 | 1323 | ); |
| 1324 | 1324 | } |
| 1325 | 1325 | prompt_for_user_comment(pComment, &prompt); |
| 1326 | 1326 | blob_reset(&prompt); |
| 1327 | 1327 | } |
| 1328 | + | |
| 1329 | +/* | |
| 1330 | +** Prepare text that describes a pending commit and write it into | |
| 1331 | +** a file at the root of the check-in. Return the name of that file. | |
| 1332 | +** | |
| 1333 | +** Space to hold the returned filename is obtained from fossil_malloc() | |
| 1334 | +** and should be freed by the caller. The caller should also unlink | |
| 1335 | +** the file when it is done. | |
| 1336 | +*/ | |
| 1337 | +static char *prepare_commit_description_file( | |
| 1338 | + CheckinInfo *p, /* Information about this commit */ | |
| 1339 | + int parent_rid /* parent check-in */ | |
| 1340 | +){ | |
| 1341 | + Blob *pDesc; | |
| 1342 | + char *zTags; | |
| 1343 | + char *zFilename; | |
| 1344 | + Blob desc; | |
| 1345 | + unsigned int r[2]; | |
| 1346 | + blob_init(&desc, 0, 0); | |
| 1347 | + pDesc = &desc; | |
| 1348 | + blob_appendf(pDesc, "checkout %s\n", g.zLocalRoot); | |
| 1349 | + blob_appendf(pDesc, "repository %s\n", g.zRepositoryName); | |
| 1350 | + blob_appendf(pDesc, "user %s\n", | |
| 1351 | + p->zUserOvrd ? p->zUserOvrd : login_name()); | |
| 1352 | + blob_appendf(pDesc, "branch %s\n", | |
| 1353 | + (p->zBranch && p->zBranch[0]) ? p->zBranch : "trunk"); | |
| 1354 | + zTags = info_tags_of_checkin(parent_rid, 1); | |
| 1355 | + if( zTags || p->azTag ){ | |
| 1356 | + blob_append(pDesc, "tags ", -1); | |
| 1357 | + if(zTags){ | |
| 1358 | + blob_appendf(pDesc, "%z%s", zTags, p->azTag ? ", " : ""); | |
| 1359 | + } | |
| 1360 | + if(p->azTag){ | |
| 1361 | + int i = 0; | |
| 1362 | + for( ; p->azTag[i]; ++i ){ | |
| 1363 | + blob_appendf(pDesc, "%s%s", p->azTag[i], | |
| 1364 | + p->azTag[i+1] ? ", " : ""); | |
| 1365 | + } | |
| 1366 | + } | |
| 1367 | + blob_appendf(pDesc, "\n"); | |
| 1368 | + } | |
| 1369 | + status_report(pDesc, C_DEFAULT | C_FATAL); | |
| 1370 | + if( g.markPrivate ){ | |
| 1371 | + blob_append(pDesc, "private-branch\n", -1); | |
| 1372 | + } | |
| 1373 | + if( p->integrateFlag ){ | |
| 1374 | + blob_append(pDesc, "integrate\n", -1); | |
| 1375 | + } | |
| 1376 | + sqlite3_randomness(sizeof(r), r); | |
| 1377 | + zFilename = mprintf("%scommit-description-%08x%08x.txt", | |
| 1378 | + g.zLocalRoot, r[0], r[1]); | |
| 1379 | + blob_write_to_file(pDesc, zFilename); | |
| 1380 | + blob_reset(pDesc); | |
| 1381 | + return zFilename; | |
| 1382 | +} | |
| 1383 | + | |
| 1328 | 1384 | |
| 1329 | 1385 | /* |
| 1330 | 1386 | ** Populate the Global.aCommitFile[] based on the command line arguments |
| 1331 | 1387 | ** to a [commit] command. Global.aCommitFile is an array of integers |
| 1332 | 1388 | ** sized at (N+1), where N is the number of arguments passed to [commit]. |
| @@ -2008,27 +2064,29 @@ | ||
| 2008 | 2064 | ** --baseline use a baseline manifest in the commit process |
| 2009 | 2065 | ** --bgcolor COLOR apply COLOR to this one check-in only |
| 2010 | 2066 | ** --branch NEW-BRANCH-NAME check in to this new branch |
| 2011 | 2067 | ** --branchcolor COLOR apply given COLOR to the branch |
| 2012 | 2068 | ** --close close the branch being committed |
| 2069 | +** --date-override DATETIME DATE to use instead of 'now' | |
| 2013 | 2070 | ** --delta use a delta manifest in the commit process |
| 2071 | +** --hash verify file status using hashing rather | |
| 2072 | +** than relying on file mtimes | |
| 2014 | 2073 | ** --integrate close all merged-in branches |
| 2015 | 2074 | ** -m|--comment COMMENT-TEXT use COMMENT-TEXT as commit comment |
| 2016 | 2075 | ** -M|--message-file FILE read the commit comment from given file |
| 2017 | 2076 | ** --mimetype MIMETYPE mimetype of check-in comment |
| 2018 | 2077 | ** -n|--dry-run If given, display instead of run actions |
| 2019 | 2078 | ** --no-prompt This option disables prompting the user for |
| 2020 | 2079 | ** input and assumes an answer of 'No' for every |
| 2021 | 2080 | ** question. |
| 2022 | 2081 | ** --no-warnings omit all warnings about file contents |
| 2082 | +** --no-verify do not run before-commit hooks | |
| 2023 | 2083 | ** --nosign do not attempt to sign this commit with gpg |
| 2024 | 2084 | ** --override-lock allow a check-in even though parent is locked |
| 2025 | 2085 | ** --private do not sync changes and their descendants |
| 2026 | -** --hash verify file status using hashing rather | |
| 2027 | -** than relying on file mtimes | |
| 2028 | 2086 | ** --tag TAG-NAME assign given tag TAG-NAME to the check-in |
| 2029 | -** --date-override DATETIME DATE to use instead of 'now' | |
| 2087 | +** --trace debug tracing. | |
| 2030 | 2088 | ** --user-override USER USER to use instead of the current default |
| 2031 | 2089 | ** |
| 2032 | 2090 | ** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in |
| 2033 | 2091 | ** year-month-day form, it may be truncated, the "T" may be replaced by |
| 2034 | 2092 | ** a space, and it may also name a timezone offset from UTC as "-HH:MM" |
| @@ -2050,10 +2108,12 @@ | ||
| 2050 | 2108 | int noSign = 0; /* True to omit signing the manifest using GPG */ |
| 2051 | 2109 | int privateFlag = 0; /* True if the --private option is present */ |
| 2052 | 2110 | int privateParent = 0; /* True if the parent check-in is private */ |
| 2053 | 2111 | int isAMerge = 0; /* True if checking in a merge */ |
| 2054 | 2112 | int noWarningFlag = 0; /* True if skipping all warnings */ |
| 2113 | + int noVerify = 0; /* Do not run before-commit hooks */ | |
| 2114 | + int bTrace = 0; /* Debug tracing */ | |
| 2055 | 2115 | int noPrompt = 0; /* True if skipping all prompts */ |
| 2056 | 2116 | int forceFlag = 0; /* Undocumented: Disables all checks */ |
| 2057 | 2117 | int forceDelta = 0; /* Force a delta-manifest */ |
| 2058 | 2118 | int forceBaseline = 0; /* Force a baseline-manifest */ |
| 2059 | 2119 | int allowConflict = 0; /* Allow unresolve merge conflicts */ |
| @@ -2109,10 +2169,12 @@ | ||
| 2109 | 2169 | allowFork = find_option("allow-fork",0,0)!=0; |
| 2110 | 2170 | if( find_option("override-lock",0,0)!=0 ) allowFork = 1; |
| 2111 | 2171 | allowOlder = find_option("allow-older",0,0)!=0; |
| 2112 | 2172 | noPrompt = find_option("no-prompt", 0, 0)!=0; |
| 2113 | 2173 | noWarningFlag = find_option("no-warnings", 0, 0)!=0; |
| 2174 | + noVerify = find_option("no-verify",0,0)!=0; | |
| 2175 | + bTrace = find_option("trace",0,0)!=0; | |
| 2114 | 2176 | sCiInfo.zBranch = find_option("branch","b",1); |
| 2115 | 2177 | sCiInfo.zColor = find_option("bgcolor",0,1); |
| 2116 | 2178 | sCiInfo.zBrClr = find_option("branchcolor",0,1); |
| 2117 | 2179 | sCiInfo.closeFlag = find_option("close",0,0)!=0; |
| 2118 | 2180 | sCiInfo.integrateFlag = find_option("integrate",0,0)!=0; |
| @@ -2340,10 +2402,21 @@ | ||
| 2340 | 2402 | fossil_fatal("cannot commit against a closed leaf"); |
| 2341 | 2403 | } |
| 2342 | 2404 | |
| 2343 | 2405 | /* Always exit the loop on the second pass */ |
| 2344 | 2406 | if( bRecheck ) break; |
| 2407 | + | |
| 2408 | + /* Run before-commit hooks */ | |
| 2409 | + if( !noVerify ){ | |
| 2410 | + char *zAuxFile = prepare_commit_description_file(&sCiInfo,vid); | |
| 2411 | + int rc = hook_run("before-commit",zAuxFile,bTrace); | |
| 2412 | + file_delete(zAuxFile); | |
| 2413 | + fossil_free(zAuxFile); | |
| 2414 | + if( rc ){ | |
| 2415 | + fossil_fatal("Before-commit hook failed\n"); | |
| 2416 | + } | |
| 2417 | + } | |
| 2345 | 2418 | |
| 2346 | 2419 | /* Get the check-in comment. This might involve prompting the |
| 2347 | 2420 | ** user for the check-in comment, in which case we should resync |
| 2348 | 2421 | ** to renew the check-in lock and repeat the checks for conflicts. |
| 2349 | 2422 | */ |
| 2350 | 2423 |
| --- src/checkin.c | |
| +++ src/checkin.c | |
| @@ -1323,10 +1323,66 @@ | |
| 1323 | ); |
| 1324 | } |
| 1325 | prompt_for_user_comment(pComment, &prompt); |
| 1326 | blob_reset(&prompt); |
| 1327 | } |
| 1328 | |
| 1329 | /* |
| 1330 | ** Populate the Global.aCommitFile[] based on the command line arguments |
| 1331 | ** to a [commit] command. Global.aCommitFile is an array of integers |
| 1332 | ** sized at (N+1), where N is the number of arguments passed to [commit]. |
| @@ -2008,27 +2064,29 @@ | |
| 2008 | ** --baseline use a baseline manifest in the commit process |
| 2009 | ** --bgcolor COLOR apply COLOR to this one check-in only |
| 2010 | ** --branch NEW-BRANCH-NAME check in to this new branch |
| 2011 | ** --branchcolor COLOR apply given COLOR to the branch |
| 2012 | ** --close close the branch being committed |
| 2013 | ** --delta use a delta manifest in the commit process |
| 2014 | ** --integrate close all merged-in branches |
| 2015 | ** -m|--comment COMMENT-TEXT use COMMENT-TEXT as commit comment |
| 2016 | ** -M|--message-file FILE read the commit comment from given file |
| 2017 | ** --mimetype MIMETYPE mimetype of check-in comment |
| 2018 | ** -n|--dry-run If given, display instead of run actions |
| 2019 | ** --no-prompt This option disables prompting the user for |
| 2020 | ** input and assumes an answer of 'No' for every |
| 2021 | ** question. |
| 2022 | ** --no-warnings omit all warnings about file contents |
| 2023 | ** --nosign do not attempt to sign this commit with gpg |
| 2024 | ** --override-lock allow a check-in even though parent is locked |
| 2025 | ** --private do not sync changes and their descendants |
| 2026 | ** --hash verify file status using hashing rather |
| 2027 | ** than relying on file mtimes |
| 2028 | ** --tag TAG-NAME assign given tag TAG-NAME to the check-in |
| 2029 | ** --date-override DATETIME DATE to use instead of 'now' |
| 2030 | ** --user-override USER USER to use instead of the current default |
| 2031 | ** |
| 2032 | ** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in |
| 2033 | ** year-month-day form, it may be truncated, the "T" may be replaced by |
| 2034 | ** a space, and it may also name a timezone offset from UTC as "-HH:MM" |
| @@ -2050,10 +2108,12 @@ | |
| 2050 | int noSign = 0; /* True to omit signing the manifest using GPG */ |
| 2051 | int privateFlag = 0; /* True if the --private option is present */ |
| 2052 | int privateParent = 0; /* True if the parent check-in is private */ |
| 2053 | int isAMerge = 0; /* True if checking in a merge */ |
| 2054 | int noWarningFlag = 0; /* True if skipping all warnings */ |
| 2055 | int noPrompt = 0; /* True if skipping all prompts */ |
| 2056 | int forceFlag = 0; /* Undocumented: Disables all checks */ |
| 2057 | int forceDelta = 0; /* Force a delta-manifest */ |
| 2058 | int forceBaseline = 0; /* Force a baseline-manifest */ |
| 2059 | int allowConflict = 0; /* Allow unresolve merge conflicts */ |
| @@ -2109,10 +2169,12 @@ | |
| 2109 | allowFork = find_option("allow-fork",0,0)!=0; |
| 2110 | if( find_option("override-lock",0,0)!=0 ) allowFork = 1; |
| 2111 | allowOlder = find_option("allow-older",0,0)!=0; |
| 2112 | noPrompt = find_option("no-prompt", 0, 0)!=0; |
| 2113 | noWarningFlag = find_option("no-warnings", 0, 0)!=0; |
| 2114 | sCiInfo.zBranch = find_option("branch","b",1); |
| 2115 | sCiInfo.zColor = find_option("bgcolor",0,1); |
| 2116 | sCiInfo.zBrClr = find_option("branchcolor",0,1); |
| 2117 | sCiInfo.closeFlag = find_option("close",0,0)!=0; |
| 2118 | sCiInfo.integrateFlag = find_option("integrate",0,0)!=0; |
| @@ -2340,10 +2402,21 @@ | |
| 2340 | fossil_fatal("cannot commit against a closed leaf"); |
| 2341 | } |
| 2342 | |
| 2343 | /* Always exit the loop on the second pass */ |
| 2344 | if( bRecheck ) break; |
| 2345 | |
| 2346 | /* Get the check-in comment. This might involve prompting the |
| 2347 | ** user for the check-in comment, in which case we should resync |
| 2348 | ** to renew the check-in lock and repeat the checks for conflicts. |
| 2349 | */ |
| 2350 |
| --- src/checkin.c | |
| +++ src/checkin.c | |
| @@ -1323,10 +1323,66 @@ | |
| 1323 | ); |
| 1324 | } |
| 1325 | prompt_for_user_comment(pComment, &prompt); |
| 1326 | blob_reset(&prompt); |
| 1327 | } |
| 1328 | |
| 1329 | /* |
| 1330 | ** Prepare text that describes a pending commit and write it into |
| 1331 | ** a file at the root of the check-in. Return the name of that file. |
| 1332 | ** |
| 1333 | ** Space to hold the returned filename is obtained from fossil_malloc() |
| 1334 | ** and should be freed by the caller. The caller should also unlink |
| 1335 | ** the file when it is done. |
| 1336 | */ |
| 1337 | static char *prepare_commit_description_file( |
| 1338 | CheckinInfo *p, /* Information about this commit */ |
| 1339 | int parent_rid /* parent check-in */ |
| 1340 | ){ |
| 1341 | Blob *pDesc; |
| 1342 | char *zTags; |
| 1343 | char *zFilename; |
| 1344 | Blob desc; |
| 1345 | unsigned int r[2]; |
| 1346 | blob_init(&desc, 0, 0); |
| 1347 | pDesc = &desc; |
| 1348 | blob_appendf(pDesc, "checkout %s\n", g.zLocalRoot); |
| 1349 | blob_appendf(pDesc, "repository %s\n", g.zRepositoryName); |
| 1350 | blob_appendf(pDesc, "user %s\n", |
| 1351 | p->zUserOvrd ? p->zUserOvrd : login_name()); |
| 1352 | blob_appendf(pDesc, "branch %s\n", |
| 1353 | (p->zBranch && p->zBranch[0]) ? p->zBranch : "trunk"); |
| 1354 | zTags = info_tags_of_checkin(parent_rid, 1); |
| 1355 | if( zTags || p->azTag ){ |
| 1356 | blob_append(pDesc, "tags ", -1); |
| 1357 | if(zTags){ |
| 1358 | blob_appendf(pDesc, "%z%s", zTags, p->azTag ? ", " : ""); |
| 1359 | } |
| 1360 | if(p->azTag){ |
| 1361 | int i = 0; |
| 1362 | for( ; p->azTag[i]; ++i ){ |
| 1363 | blob_appendf(pDesc, "%s%s", p->azTag[i], |
| 1364 | p->azTag[i+1] ? ", " : ""); |
| 1365 | } |
| 1366 | } |
| 1367 | blob_appendf(pDesc, "\n"); |
| 1368 | } |
| 1369 | status_report(pDesc, C_DEFAULT | C_FATAL); |
| 1370 | if( g.markPrivate ){ |
| 1371 | blob_append(pDesc, "private-branch\n", -1); |
| 1372 | } |
| 1373 | if( p->integrateFlag ){ |
| 1374 | blob_append(pDesc, "integrate\n", -1); |
| 1375 | } |
| 1376 | sqlite3_randomness(sizeof(r), r); |
| 1377 | zFilename = mprintf("%scommit-description-%08x%08x.txt", |
| 1378 | g.zLocalRoot, r[0], r[1]); |
| 1379 | blob_write_to_file(pDesc, zFilename); |
| 1380 | blob_reset(pDesc); |
| 1381 | return zFilename; |
| 1382 | } |
| 1383 | |
| 1384 | |
| 1385 | /* |
| 1386 | ** Populate the Global.aCommitFile[] based on the command line arguments |
| 1387 | ** to a [commit] command. Global.aCommitFile is an array of integers |
| 1388 | ** sized at (N+1), where N is the number of arguments passed to [commit]. |
| @@ -2008,27 +2064,29 @@ | |
| 2064 | ** --baseline use a baseline manifest in the commit process |
| 2065 | ** --bgcolor COLOR apply COLOR to this one check-in only |
| 2066 | ** --branch NEW-BRANCH-NAME check in to this new branch |
| 2067 | ** --branchcolor COLOR apply given COLOR to the branch |
| 2068 | ** --close close the branch being committed |
| 2069 | ** --date-override DATETIME DATE to use instead of 'now' |
| 2070 | ** --delta use a delta manifest in the commit process |
| 2071 | ** --hash verify file status using hashing rather |
| 2072 | ** than relying on file mtimes |
| 2073 | ** --integrate close all merged-in branches |
| 2074 | ** -m|--comment COMMENT-TEXT use COMMENT-TEXT as commit comment |
| 2075 | ** -M|--message-file FILE read the commit comment from given file |
| 2076 | ** --mimetype MIMETYPE mimetype of check-in comment |
| 2077 | ** -n|--dry-run If given, display instead of run actions |
| 2078 | ** --no-prompt This option disables prompting the user for |
| 2079 | ** input and assumes an answer of 'No' for every |
| 2080 | ** question. |
| 2081 | ** --no-warnings omit all warnings about file contents |
| 2082 | ** --no-verify do not run before-commit hooks |
| 2083 | ** --nosign do not attempt to sign this commit with gpg |
| 2084 | ** --override-lock allow a check-in even though parent is locked |
| 2085 | ** --private do not sync changes and their descendants |
| 2086 | ** --tag TAG-NAME assign given tag TAG-NAME to the check-in |
| 2087 | ** --trace debug tracing. |
| 2088 | ** --user-override USER USER to use instead of the current default |
| 2089 | ** |
| 2090 | ** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in |
| 2091 | ** year-month-day form, it may be truncated, the "T" may be replaced by |
| 2092 | ** a space, and it may also name a timezone offset from UTC as "-HH:MM" |
| @@ -2050,10 +2108,12 @@ | |
| 2108 | int noSign = 0; /* True to omit signing the manifest using GPG */ |
| 2109 | int privateFlag = 0; /* True if the --private option is present */ |
| 2110 | int privateParent = 0; /* True if the parent check-in is private */ |
| 2111 | int isAMerge = 0; /* True if checking in a merge */ |
| 2112 | int noWarningFlag = 0; /* True if skipping all warnings */ |
| 2113 | int noVerify = 0; /* Do not run before-commit hooks */ |
| 2114 | int bTrace = 0; /* Debug tracing */ |
| 2115 | int noPrompt = 0; /* True if skipping all prompts */ |
| 2116 | int forceFlag = 0; /* Undocumented: Disables all checks */ |
| 2117 | int forceDelta = 0; /* Force a delta-manifest */ |
| 2118 | int forceBaseline = 0; /* Force a baseline-manifest */ |
| 2119 | int allowConflict = 0; /* Allow unresolve merge conflicts */ |
| @@ -2109,10 +2169,12 @@ | |
| 2169 | allowFork = find_option("allow-fork",0,0)!=0; |
| 2170 | if( find_option("override-lock",0,0)!=0 ) allowFork = 1; |
| 2171 | allowOlder = find_option("allow-older",0,0)!=0; |
| 2172 | noPrompt = find_option("no-prompt", 0, 0)!=0; |
| 2173 | noWarningFlag = find_option("no-warnings", 0, 0)!=0; |
| 2174 | noVerify = find_option("no-verify",0,0)!=0; |
| 2175 | bTrace = find_option("trace",0,0)!=0; |
| 2176 | sCiInfo.zBranch = find_option("branch","b",1); |
| 2177 | sCiInfo.zColor = find_option("bgcolor",0,1); |
| 2178 | sCiInfo.zBrClr = find_option("branchcolor",0,1); |
| 2179 | sCiInfo.closeFlag = find_option("close",0,0)!=0; |
| 2180 | sCiInfo.integrateFlag = find_option("integrate",0,0)!=0; |
| @@ -2340,10 +2402,21 @@ | |
| 2402 | fossil_fatal("cannot commit against a closed leaf"); |
| 2403 | } |
| 2404 | |
| 2405 | /* Always exit the loop on the second pass */ |
| 2406 | if( bRecheck ) break; |
| 2407 | |
| 2408 | /* Run before-commit hooks */ |
| 2409 | if( !noVerify ){ |
| 2410 | char *zAuxFile = prepare_commit_description_file(&sCiInfo,vid); |
| 2411 | int rc = hook_run("before-commit",zAuxFile,bTrace); |
| 2412 | file_delete(zAuxFile); |
| 2413 | fossil_free(zAuxFile); |
| 2414 | if( rc ){ |
| 2415 | fossil_fatal("Before-commit hook failed\n"); |
| 2416 | } |
| 2417 | } |
| 2418 | |
| 2419 | /* Get the check-in comment. This might involve prompting the |
| 2420 | ** user for the check-in comment, in which case we should resync |
| 2421 | ** to renew the check-in lock and repeat the checks for conflicts. |
| 2422 | */ |
| 2423 |
+52
-6
| --- src/hook.c | ||
| +++ src/hook.c | ||
| @@ -79,15 +79,19 @@ | ||
| 79 | 79 | ** Translate a hook command string into its executable format by |
| 80 | 80 | ** converting every %-substitutions as follows: |
| 81 | 81 | ** |
| 82 | 82 | ** %F -> Name of the fossil executable |
| 83 | 83 | ** %R -> Name of the repository |
| 84 | +** %A -> Auxiliary information filename (might be empty string) | |
| 84 | 85 | ** |
| 85 | 86 | ** The returned string is obtained from fossil_malloc() and should |
| 86 | 87 | ** be freed by the caller. |
| 87 | 88 | */ |
| 88 | -char *hook_subst(const char *zCmd){ | |
| 89 | +static char *hook_subst( | |
| 90 | + const char *zCmd, | |
| 91 | + const char *zAuxFilename /* Name of auxiliary information file */ | |
| 92 | +){ | |
| 89 | 93 | Blob r; |
| 90 | 94 | int i; |
| 91 | 95 | blob_init(&r, 0, 0); |
| 92 | 96 | while( zCmd[0] ){ |
| 93 | 97 | for(i=0; zCmd[i] && zCmd[i]!='%'; i++){} |
| @@ -97,10 +101,13 @@ | ||
| 97 | 101 | blob_append(&r, g.nameOfExe, -1); |
| 98 | 102 | zCmd += i+2; |
| 99 | 103 | }else if( zCmd[i+1]=='R' ){ |
| 100 | 104 | blob_append(&r, g.zRepositoryName, -1); |
| 101 | 105 | zCmd += i+2; |
| 106 | + }else if( zCmd[i+1]=='A' ){ | |
| 107 | + if( zAuxFilename ) blob_append(&r, zAuxFilename, -1); | |
| 108 | + zCmd += i+2; | |
| 102 | 109 | }else{ |
| 103 | 110 | blob_append(&r, zCmd+i, 1); |
| 104 | 111 | zCmd += i+1; |
| 105 | 112 | } |
| 106 | 113 | } |
| @@ -206,13 +213,14 @@ | ||
| 206 | 213 | ** > fossil hook test [OPTIONS] ID |
| 207 | 214 | ** |
| 208 | 215 | ** Run the hook script given by ID for testing purposes. |
| 209 | 216 | ** Options: |
| 210 | 217 | ** |
| 211 | -** --dry-run Print the script on stdout rather than run it | |
| 212 | -** --base-rcvid N Pretend that the hook-last-rcvid value is N | |
| 213 | -** --new-rcvid M Pretend that the last rcvid valud is M | |
| 218 | +** --dry-run Print the script on stdout rather than run it | |
| 219 | +** --base-rcvid N Pretend that the hook-last-rcvid value is N | |
| 220 | +** --new-rcvid M Pretend that the last rcvid valud is M | |
| 221 | +** --aux-file NAME NAME is substituted for %A in the script | |
| 214 | 222 | ** |
| 215 | 223 | ** The --base-rcvid and --new-rcvid options are silently ignored if |
| 216 | 224 | ** the hook type is not "after-receive". The default values for |
| 217 | 225 | ** --base-rcvid and --new-rcvid cause the last recieve to be processed. |
| 218 | 226 | */ |
| @@ -351,10 +359,11 @@ | ||
| 351 | 359 | if( strncmp(zCmd, "test", nCmd)==0 ){ |
| 352 | 360 | Stmt q; |
| 353 | 361 | int bDryRun = find_option("dry-run", "n", 0)!=0; |
| 354 | 362 | const char *zOrigRcvid = find_option("base-rcvid",0,1); |
| 355 | 363 | const char *zNewRcvid = find_option("new-rcvid",0,1); |
| 364 | + const char *zAuxFilename = find_option("aux-file",0,1); | |
| 356 | 365 | verify_all_options(); |
| 357 | 366 | if( g.argc<4 ) usage("test ID"); |
| 358 | 367 | int id = atoi(g.argv[3]); |
| 359 | 368 | if( zOrigRcvid==0 ){ |
| 360 | 369 | zOrigRcvid = db_text(0, "SELECT max(rcvid)-1 FROM rcvfrom"); |
| @@ -366,11 +375,11 @@ | ||
| 366 | 375 | " WHERE name='hooks' AND json_valid(value)", |
| 367 | 376 | id, id |
| 368 | 377 | ); |
| 369 | 378 | while( db_step(&q)==SQLITE_ROW ){ |
| 370 | 379 | const char *zCmd = db_column_text(&q,0); |
| 371 | - char *zCmd2 = hook_subst(zCmd); | |
| 380 | + char *zCmd2 = hook_subst(zCmd, zAuxFilename); | |
| 372 | 381 | int needOut = db_column_int(&q,1); |
| 373 | 382 | Blob out; |
| 374 | 383 | blob_init(&out,0,0); |
| 375 | 384 | if( needOut ) hook_changes(&out, zOrigRcvid, zNewRcvid); |
| 376 | 385 | if( bDryRun ){ |
| @@ -432,11 +441,11 @@ | ||
| 432 | 441 | char *zCmd; |
| 433 | 442 | FILE *f; |
| 434 | 443 | if( cnt==0 ){ |
| 435 | 444 | hook_changes(&chng, zLastRcvid, 0); |
| 436 | 445 | } |
| 437 | - zCmd = hook_subst(db_column_text(&q,0)); | |
| 446 | + zCmd = hook_subst(db_column_text(&q,0), 0); | |
| 438 | 447 | f = popen(zCmd, "w"); |
| 439 | 448 | if( f ){ |
| 440 | 449 | fwrite(blob_buffer(&chng),1,blob_size(&chng),f); |
| 441 | 450 | pclose(f); |
| 442 | 451 | } |
| @@ -448,5 +457,42 @@ | ||
| 448 | 457 | blob_reset(&chng); |
| 449 | 458 | hook_backoffice_done: |
| 450 | 459 | db_commit_transaction(); |
| 451 | 460 | return cnt; |
| 452 | 461 | } |
| 462 | + | |
| 463 | +/* | |
| 464 | +** Run all hooks of type zType. Use zAuxFile as the auxiliary information | |
| 465 | +** file. | |
| 466 | +** | |
| 467 | +** If any hook returns non-zero, then stop running and return non-zero. | |
| 468 | +** Return zero only if all hooks return zero. | |
| 469 | +*/ | |
| 470 | +int hook_run(const char *zType, const char *zAuxFile, int traceFlag){ | |
| 471 | + Stmt q; | |
| 472 | + int rc = 0; | |
| 473 | + if( !db_exists("SELECT 1 FROM config WHERE name='hooks'") ){ | |
| 474 | + return 0; | |
| 475 | + } | |
| 476 | + db_prepare(&q, | |
| 477 | + "SELECT json_extract(jx.value,'$.cmd') " | |
| 478 | + " FROM config, json_each(config.value) AS jx" | |
| 479 | + " WHERE config.name='hooks' AND json_valid(config.value)" | |
| 480 | + " AND json_extract(jx.value,'$.type')=%Q" | |
| 481 | + " ORDER BY json_extract(jx.value,'$.seq');", | |
| 482 | + zType | |
| 483 | + ); | |
| 484 | + while( db_step(&q)==SQLITE_ROW ){ | |
| 485 | + char *zCmd; | |
| 486 | + zCmd = hook_subst(db_column_text(&q,0), zAuxFile); | |
| 487 | + if( traceFlag ){ | |
| 488 | + fossil_print("%s hook: %s\n", zType, zCmd); | |
| 489 | + } | |
| 490 | + rc = fossil_system(zCmd); | |
| 491 | + fossil_free(zCmd); | |
| 492 | + if( rc ){ | |
| 493 | + break; | |
| 494 | + } | |
| 495 | + } | |
| 496 | + db_finalize(&q); | |
| 497 | + return rc; | |
| 498 | +} | |
| 453 | 499 |
| --- src/hook.c | |
| +++ src/hook.c | |
| @@ -79,15 +79,19 @@ | |
| 79 | ** Translate a hook command string into its executable format by |
| 80 | ** converting every %-substitutions as follows: |
| 81 | ** |
| 82 | ** %F -> Name of the fossil executable |
| 83 | ** %R -> Name of the repository |
| 84 | ** |
| 85 | ** The returned string is obtained from fossil_malloc() and should |
| 86 | ** be freed by the caller. |
| 87 | */ |
| 88 | char *hook_subst(const char *zCmd){ |
| 89 | Blob r; |
| 90 | int i; |
| 91 | blob_init(&r, 0, 0); |
| 92 | while( zCmd[0] ){ |
| 93 | for(i=0; zCmd[i] && zCmd[i]!='%'; i++){} |
| @@ -97,10 +101,13 @@ | |
| 97 | blob_append(&r, g.nameOfExe, -1); |
| 98 | zCmd += i+2; |
| 99 | }else if( zCmd[i+1]=='R' ){ |
| 100 | blob_append(&r, g.zRepositoryName, -1); |
| 101 | zCmd += i+2; |
| 102 | }else{ |
| 103 | blob_append(&r, zCmd+i, 1); |
| 104 | zCmd += i+1; |
| 105 | } |
| 106 | } |
| @@ -206,13 +213,14 @@ | |
| 206 | ** > fossil hook test [OPTIONS] ID |
| 207 | ** |
| 208 | ** Run the hook script given by ID for testing purposes. |
| 209 | ** Options: |
| 210 | ** |
| 211 | ** --dry-run Print the script on stdout rather than run it |
| 212 | ** --base-rcvid N Pretend that the hook-last-rcvid value is N |
| 213 | ** --new-rcvid M Pretend that the last rcvid valud is M |
| 214 | ** |
| 215 | ** The --base-rcvid and --new-rcvid options are silently ignored if |
| 216 | ** the hook type is not "after-receive". The default values for |
| 217 | ** --base-rcvid and --new-rcvid cause the last recieve to be processed. |
| 218 | */ |
| @@ -351,10 +359,11 @@ | |
| 351 | if( strncmp(zCmd, "test", nCmd)==0 ){ |
| 352 | Stmt q; |
| 353 | int bDryRun = find_option("dry-run", "n", 0)!=0; |
| 354 | const char *zOrigRcvid = find_option("base-rcvid",0,1); |
| 355 | const char *zNewRcvid = find_option("new-rcvid",0,1); |
| 356 | verify_all_options(); |
| 357 | if( g.argc<4 ) usage("test ID"); |
| 358 | int id = atoi(g.argv[3]); |
| 359 | if( zOrigRcvid==0 ){ |
| 360 | zOrigRcvid = db_text(0, "SELECT max(rcvid)-1 FROM rcvfrom"); |
| @@ -366,11 +375,11 @@ | |
| 366 | " WHERE name='hooks' AND json_valid(value)", |
| 367 | id, id |
| 368 | ); |
| 369 | while( db_step(&q)==SQLITE_ROW ){ |
| 370 | const char *zCmd = db_column_text(&q,0); |
| 371 | char *zCmd2 = hook_subst(zCmd); |
| 372 | int needOut = db_column_int(&q,1); |
| 373 | Blob out; |
| 374 | blob_init(&out,0,0); |
| 375 | if( needOut ) hook_changes(&out, zOrigRcvid, zNewRcvid); |
| 376 | if( bDryRun ){ |
| @@ -432,11 +441,11 @@ | |
| 432 | char *zCmd; |
| 433 | FILE *f; |
| 434 | if( cnt==0 ){ |
| 435 | hook_changes(&chng, zLastRcvid, 0); |
| 436 | } |
| 437 | zCmd = hook_subst(db_column_text(&q,0)); |
| 438 | f = popen(zCmd, "w"); |
| 439 | if( f ){ |
| 440 | fwrite(blob_buffer(&chng),1,blob_size(&chng),f); |
| 441 | pclose(f); |
| 442 | } |
| @@ -448,5 +457,42 @@ | |
| 448 | blob_reset(&chng); |
| 449 | hook_backoffice_done: |
| 450 | db_commit_transaction(); |
| 451 | return cnt; |
| 452 | } |
| 453 |
| --- src/hook.c | |
| +++ src/hook.c | |
| @@ -79,15 +79,19 @@ | |
| 79 | ** Translate a hook command string into its executable format by |
| 80 | ** converting every %-substitutions as follows: |
| 81 | ** |
| 82 | ** %F -> Name of the fossil executable |
| 83 | ** %R -> Name of the repository |
| 84 | ** %A -> Auxiliary information filename (might be empty string) |
| 85 | ** |
| 86 | ** The returned string is obtained from fossil_malloc() and should |
| 87 | ** be freed by the caller. |
| 88 | */ |
| 89 | static char *hook_subst( |
| 90 | const char *zCmd, |
| 91 | const char *zAuxFilename /* Name of auxiliary information file */ |
| 92 | ){ |
| 93 | Blob r; |
| 94 | int i; |
| 95 | blob_init(&r, 0, 0); |
| 96 | while( zCmd[0] ){ |
| 97 | for(i=0; zCmd[i] && zCmd[i]!='%'; i++){} |
| @@ -97,10 +101,13 @@ | |
| 101 | blob_append(&r, g.nameOfExe, -1); |
| 102 | zCmd += i+2; |
| 103 | }else if( zCmd[i+1]=='R' ){ |
| 104 | blob_append(&r, g.zRepositoryName, -1); |
| 105 | zCmd += i+2; |
| 106 | }else if( zCmd[i+1]=='A' ){ |
| 107 | if( zAuxFilename ) blob_append(&r, zAuxFilename, -1); |
| 108 | zCmd += i+2; |
| 109 | }else{ |
| 110 | blob_append(&r, zCmd+i, 1); |
| 111 | zCmd += i+1; |
| 112 | } |
| 113 | } |
| @@ -206,13 +213,14 @@ | |
| 213 | ** > fossil hook test [OPTIONS] ID |
| 214 | ** |
| 215 | ** Run the hook script given by ID for testing purposes. |
| 216 | ** Options: |
| 217 | ** |
| 218 | ** --dry-run Print the script on stdout rather than run it |
| 219 | ** --base-rcvid N Pretend that the hook-last-rcvid value is N |
| 220 | ** --new-rcvid M Pretend that the last rcvid valud is M |
| 221 | ** --aux-file NAME NAME is substituted for %A in the script |
| 222 | ** |
| 223 | ** The --base-rcvid and --new-rcvid options are silently ignored if |
| 224 | ** the hook type is not "after-receive". The default values for |
| 225 | ** --base-rcvid and --new-rcvid cause the last recieve to be processed. |
| 226 | */ |
| @@ -351,10 +359,11 @@ | |
| 359 | if( strncmp(zCmd, "test", nCmd)==0 ){ |
| 360 | Stmt q; |
| 361 | int bDryRun = find_option("dry-run", "n", 0)!=0; |
| 362 | const char *zOrigRcvid = find_option("base-rcvid",0,1); |
| 363 | const char *zNewRcvid = find_option("new-rcvid",0,1); |
| 364 | const char *zAuxFilename = find_option("aux-file",0,1); |
| 365 | verify_all_options(); |
| 366 | if( g.argc<4 ) usage("test ID"); |
| 367 | int id = atoi(g.argv[3]); |
| 368 | if( zOrigRcvid==0 ){ |
| 369 | zOrigRcvid = db_text(0, "SELECT max(rcvid)-1 FROM rcvfrom"); |
| @@ -366,11 +375,11 @@ | |
| 375 | " WHERE name='hooks' AND json_valid(value)", |
| 376 | id, id |
| 377 | ); |
| 378 | while( db_step(&q)==SQLITE_ROW ){ |
| 379 | const char *zCmd = db_column_text(&q,0); |
| 380 | char *zCmd2 = hook_subst(zCmd, zAuxFilename); |
| 381 | int needOut = db_column_int(&q,1); |
| 382 | Blob out; |
| 383 | blob_init(&out,0,0); |
| 384 | if( needOut ) hook_changes(&out, zOrigRcvid, zNewRcvid); |
| 385 | if( bDryRun ){ |
| @@ -432,11 +441,11 @@ | |
| 441 | char *zCmd; |
| 442 | FILE *f; |
| 443 | if( cnt==0 ){ |
| 444 | hook_changes(&chng, zLastRcvid, 0); |
| 445 | } |
| 446 | zCmd = hook_subst(db_column_text(&q,0), 0); |
| 447 | f = popen(zCmd, "w"); |
| 448 | if( f ){ |
| 449 | fwrite(blob_buffer(&chng),1,blob_size(&chng),f); |
| 450 | pclose(f); |
| 451 | } |
| @@ -448,5 +457,42 @@ | |
| 457 | blob_reset(&chng); |
| 458 | hook_backoffice_done: |
| 459 | db_commit_transaction(); |
| 460 | return cnt; |
| 461 | } |
| 462 | |
| 463 | /* |
| 464 | ** Run all hooks of type zType. Use zAuxFile as the auxiliary information |
| 465 | ** file. |
| 466 | ** |
| 467 | ** If any hook returns non-zero, then stop running and return non-zero. |
| 468 | ** Return zero only if all hooks return zero. |
| 469 | */ |
| 470 | int hook_run(const char *zType, const char *zAuxFile, int traceFlag){ |
| 471 | Stmt q; |
| 472 | int rc = 0; |
| 473 | if( !db_exists("SELECT 1 FROM config WHERE name='hooks'") ){ |
| 474 | return 0; |
| 475 | } |
| 476 | db_prepare(&q, |
| 477 | "SELECT json_extract(jx.value,'$.cmd') " |
| 478 | " FROM config, json_each(config.value) AS jx" |
| 479 | " WHERE config.name='hooks' AND json_valid(config.value)" |
| 480 | " AND json_extract(jx.value,'$.type')=%Q" |
| 481 | " ORDER BY json_extract(jx.value,'$.seq');", |
| 482 | zType |
| 483 | ); |
| 484 | while( db_step(&q)==SQLITE_ROW ){ |
| 485 | char *zCmd; |
| 486 | zCmd = hook_subst(db_column_text(&q,0), zAuxFile); |
| 487 | if( traceFlag ){ |
| 488 | fossil_print("%s hook: %s\n", zType, zCmd); |
| 489 | } |
| 490 | rc = fossil_system(zCmd); |
| 491 | fossil_free(zCmd); |
| 492 | if( rc ){ |
| 493 | break; |
| 494 | } |
| 495 | } |
| 496 | db_finalize(&q); |
| 497 | return rc; |
| 498 | } |
| 499 |