1 /* hooks.c : running repository hooks
3 * ====================================================================
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
20 * ====================================================================
27 #include <apr_pools.h>
28 #include <apr_file_io.h>
30 #include "svn_config.h"
32 #include "svn_error.h"
33 #include "svn_dirent_uri.h"
35 #include "svn_pools.h"
36 #include "svn_repos.h"
39 #include "svn_private_config.h"
40 #include "private/svn_fs_private.h"
41 #include "private/svn_repos_private.h"
42 #include "private/svn_string_private.h"
46 /*** Hook drivers. ***/
48 /* Helper function for run_hook_cmd(). Wait for a hook to finish
49 executing and return either SVN_NO_ERROR if the hook script completed
50 without error, or an error describing the reason for failure.
52 NAME and CMD are the name and path of the hook program, CMD_PROC
53 is a pointer to the structure representing the running process,
54 and READ_ERRHANDLE is an open handle to the hook's stderr.
56 Hooks are considered to have failed if we are unable to wait for the
57 process, if we are unable to read from the hook's stderr, if the
58 process has failed to exit cleanly (due to a coredump, for example),
59 or if the process returned a non-zero return code.
61 Any error output returned by the hook's stderr will be included in an
62 error message, though the presence of output on stderr is not itself
63 a reason to fail a hook. */
65 check_hook_result(const char *name, const char *cmd, apr_proc_t *cmd_proc,
66 apr_file_t *read_errhandle, apr_pool_t *pool)
68 svn_error_t *err, *err2;
69 svn_stringbuf_t *native_stderr, *failure_message;
70 const char *utf8_stderr;
72 apr_exit_why_e exitwhy;
74 err2 = svn_stringbuf_from_aprfile(&native_stderr, read_errhandle, pool);
76 err = svn_io_wait_for_cmd(cmd_proc, cmd, &exitcode, &exitwhy, pool);
79 svn_error_clear(err2);
80 return svn_error_trace(err);
83 if (APR_PROC_CHECK_EXIT(exitwhy) && exitcode == 0)
85 /* The hook exited cleanly. However, if we got an error reading
86 the hook's stderr, fail the hook anyway, because this might be
87 symptomatic of a more important problem. */
90 return svn_error_createf
91 (SVN_ERR_REPOS_HOOK_FAILURE, err2,
92 _("'%s' hook succeeded, but error output could not be read"),
99 /* The hook script failed. */
101 /* If we got the stderr output okay, try to translate it into UTF-8.
102 Ensure there is something sensible in the UTF-8 string regardless. */
105 err2 = svn_utf_cstring_to_utf8(&utf8_stderr, native_stderr->data, pool);
107 utf8_stderr = _("[Error output could not be translated from the "
108 "native locale to UTF-8.]");
112 utf8_stderr = _("[Error output could not be read.]");
114 /*### It would be nice to include the text of any translation or read
115 error in the messages above before we clear it here. */
116 svn_error_clear(err2);
118 if (!APR_PROC_CHECK_EXIT(exitwhy))
120 failure_message = svn_stringbuf_createf(pool,
121 _("'%s' hook failed (did not exit cleanly: "
122 "apr_exit_why_e was %d, exitcode was %d). "),
123 name, exitwhy, exitcode);
128 if (strcmp(name, "start-commit") == 0
129 || strcmp(name, "pre-commit") == 0)
130 action = _("Commit");
131 else if (strcmp(name, "pre-revprop-change") == 0)
132 action = _("Revprop change");
133 else if (strcmp(name, "pre-lock") == 0)
135 else if (strcmp(name, "pre-unlock") == 0)
136 action = _("Unlock");
140 failure_message = svn_stringbuf_createf(
141 pool, _("%s hook failed (exit code %d)"),
144 failure_message = svn_stringbuf_createf(
145 pool, _("%s blocked by %s hook (exit code %d)"),
146 action, name, exitcode);
151 svn_stringbuf_appendcstr(failure_message,
152 _(" with output:\n"));
153 svn_stringbuf_appendcstr(failure_message, utf8_stderr);
157 svn_stringbuf_appendcstr(failure_message,
158 _(" with no output."));
161 return svn_error_create(SVN_ERR_REPOS_HOOK_FAILURE, err,
162 failure_message->data);
165 /* Copy the environment given as key/value pairs of ENV_HASH into
166 * an array of C strings allocated in RESULT_POOL.
167 * If the hook environment is empty, return NULL.
168 * Use SCRATCH_POOL for temporary allocations. */
170 env_from_env_hash(apr_hash_t *env_hash,
171 apr_pool_t *result_pool,
172 apr_pool_t *scratch_pool)
174 apr_hash_index_t *hi;
181 env = apr_palloc(result_pool,
182 sizeof(const char *) * (apr_hash_count(env_hash) + 1));
184 for (hi = apr_hash_first(scratch_pool, env_hash); hi; hi = apr_hash_next(hi))
186 *envp = apr_psprintf(result_pool, "%s=%s",
187 (const char *)apr_hash_this_key(hi),
188 (const char *)apr_hash_this_val(hi));
196 /* NAME, CMD and ARGS are the name, path to and arguments for the hook
197 program that is to be run. The hook's exit status will be checked,
198 and if an error occurred the hook's stderr output will be added to
201 If STDIN_HANDLE is non-null, pass it as the hook's stdin, else pass
202 no stdin to the hook.
204 If RESULT is non-null, set *RESULT to the stdout of the hook or to
205 a zero-length string if the hook generates no output on stdout. */
207 run_hook_cmd(svn_string_t **result,
211 apr_hash_t *hooks_env,
212 apr_file_t *stdin_handle,
215 apr_file_t *null_handle;
216 apr_status_t apr_err;
218 apr_proc_t cmd_proc = {0};
219 apr_pool_t *cmd_pool;
220 apr_hash_t *hook_env = NULL;
228 /* Redirect stdout to the null device */
229 apr_err = apr_file_open(&null_handle, SVN_NULL_DEVICE_NAME, APR_WRITE,
230 APR_OS_DEFAULT, pool);
232 return svn_error_wrap_apr
233 (apr_err, _("Can't create null stdout for hook '%s'"), cmd);
236 /* Tie resources allocated for the command to a special pool which we can
237 * destroy in order to clean up the stderr pipe opened for the process. */
238 cmd_pool = svn_pool_create(pool);
240 /* Check if a custom environment is defined for this hook, or else
241 * whether a default environment is defined. */
244 hook_env = svn_hash_gets(hooks_env, name);
245 if (hook_env == NULL)
246 hook_env = svn_hash_gets(hooks_env,
247 SVN_REPOS__HOOKS_ENV_DEFAULT_SECTION);
250 err = svn_io_start_cmd3(&cmd_proc, ".", cmd, args,
251 env_from_env_hash(hook_env, pool, pool),
252 FALSE, FALSE, stdin_handle, result != NULL,
253 null_handle, TRUE, NULL, cmd_pool);
255 err = check_hook_result(name, cmd, &cmd_proc, cmd_proc.err, pool);
258 /* The command could not be started for some reason. */
259 err = svn_error_createf(SVN_ERR_REPOS_BAD_ARGS, err,
260 _("Failed to start '%s' hook"), cmd);
263 /* Hooks are fallible, and so hook failure is "expected" to occur at
264 times. When such a failure happens we still want to close the pipe
268 svn_stringbuf_t *native_stdout;
269 err = svn_stringbuf_from_aprfile(&native_stdout, cmd_proc.out, pool);
271 *result = svn_stringbuf__morph_into_string(native_stdout);
274 /* Close resources allocated by svn_io_start_cmd3(), such as the pipe. */
275 svn_pool_destroy(cmd_pool);
277 /* Close the null handle. */
280 apr_err = apr_file_close(null_handle);
282 return svn_error_wrap_apr(apr_err, _("Error closing null file"));
285 return svn_error_trace(err);
289 /* Create a temporary file F that will automatically be deleted when the
290 pool is cleaned up. Fill it with VALUE, and leave it open and rewound,
291 ready to be read from. */
293 create_temp_file(apr_file_t **f, const svn_string_t *value, apr_pool_t *pool)
295 apr_off_t offset = 0;
297 SVN_ERR(svn_io_open_unique_file3(f, NULL, NULL,
298 svn_io_file_del_on_pool_cleanup,
300 SVN_ERR(svn_io_file_write_full(*f, value->data, value->len, NULL, pool));
301 return svn_io_file_seek(*f, APR_SET, &offset, pool);
305 /* Check if the HOOK program exists and is a file or a symbolic link, using
306 POOL for temporary allocations.
308 If the hook exists but is a broken symbolic link, set *BROKEN_LINK
309 to TRUE, else if the hook program exists set *BROKEN_LINK to FALSE.
311 Return the hook program if found, else return NULL and don't touch
315 check_hook_cmd(const char *hook, svn_boolean_t *broken_link, apr_pool_t *pool)
317 static const char* const check_extns[] = {
319 /* For WIN32, we need to check with file name extension(s) added.
321 As Windows Scripting Host (.wsf) files can accommodate (at least)
322 JavaScript (.js) and VB Script (.vbs) code, extensions for the
323 corresponding file types need not be enumerated explicitly. */
324 ".exe", ".cmd", ".bat", ".wsf", /* ### Any other extensions? */
331 const char *const *extn;
332 svn_error_t *err = NULL;
333 svn_boolean_t is_special;
334 for (extn = check_extns; *extn; ++extn)
336 const char *const hook_path =
337 (**extn ? apr_pstrcat(pool, hook, *extn, SVN_VA_NULL) : hook);
339 svn_node_kind_t kind;
340 if (!(err = svn_io_check_resolved_path(hook_path, &kind, pool))
341 && kind == svn_node_file)
343 *broken_link = FALSE;
346 svn_error_clear(err);
347 if (!(err = svn_io_check_special_path(hook_path, &kind, &is_special,
354 svn_error_clear(err);
359 /* Baton for parse_hooks_env_option. */
360 struct parse_hooks_env_option_baton {
361 /* The name of the section being parsed. If not the default section,
362 * the section name should match the name of a hook to which the
365 apr_hash_t *hooks_env;
368 /* An implementation of svn_config_enumerator2_t.
369 * Set environment variable NAME to value VALUE in the environment for
370 * all hooks (in case the current section is the default section),
371 * or the hook with the name corresponding to the current section's name. */
373 parse_hooks_env_option(const char *name, const char *value,
374 void *baton, apr_pool_t *pool)
376 struct parse_hooks_env_option_baton *bo = baton;
377 apr_pool_t *result_pool = apr_hash_pool_get(bo->hooks_env);
378 apr_hash_t *hook_env;
380 hook_env = svn_hash_gets(bo->hooks_env, bo->section);
381 if (hook_env == NULL)
383 hook_env = apr_hash_make(result_pool);
384 svn_hash_sets(bo->hooks_env, apr_pstrdup(result_pool, bo->section),
387 svn_hash_sets(hook_env, apr_pstrdup(result_pool, name),
388 apr_pstrdup(result_pool, value));
393 struct parse_hooks_env_section_baton {
395 apr_hash_t *hooks_env;
398 /* An implementation of svn_config_section_enumerator2_t. */
400 parse_hooks_env_section(const char *name, void *baton, apr_pool_t *pool)
402 struct parse_hooks_env_section_baton *b = baton;
403 struct parse_hooks_env_option_baton bo;
406 bo.hooks_env = b->hooks_env;
408 (void)svn_config_enumerate2(b->cfg, name, parse_hooks_env_option, &bo, pool);
414 svn_repos__parse_hooks_env(apr_hash_t **hooks_env_p,
415 const char *local_abspath,
416 apr_pool_t *result_pool,
417 apr_pool_t *scratch_pool)
419 struct parse_hooks_env_section_baton b;
422 svn_node_kind_t kind;
423 SVN_ERR(svn_io_check_path(local_abspath, &kind, scratch_pool));
425 b.hooks_env = apr_hash_make(result_pool);
427 if (kind != svn_node_none)
430 SVN_ERR(svn_config_read3(&cfg, local_abspath, FALSE,
431 TRUE, TRUE, scratch_pool));
434 (void)svn_config_enumerate_sections2(cfg, parse_hooks_env_section,
438 *hooks_env_p = b.hooks_env;
448 /* Return an error for the failure of HOOK due to a broken symlink. */
450 hook_symlink_error(const char *hook)
452 return svn_error_createf
453 (SVN_ERR_REPOS_HOOK_FAILURE, NULL,
454 _("Failed to run '%s' hook; broken symlink"), hook);
458 svn_repos__hooks_start_commit(svn_repos_t *repos,
459 apr_hash_t *hooks_env,
461 const apr_array_header_t *capabilities,
462 const char *txn_name,
465 const char *hook = svn_repos_start_commit_hook(repos, pool);
466 svn_boolean_t broken_link;
468 if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link)
470 return hook_symlink_error(hook);
475 char *capabilities_string;
479 capabilities_string = svn_cstring_join(capabilities, ":", pool);
481 /* Get rid of that annoying final colon. */
482 if (capabilities_string[0])
483 capabilities_string[strlen(capabilities_string) - 1] = '\0';
487 capabilities_string = apr_pstrdup(pool, "");
491 args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool);
492 args[2] = user ? user : "";
493 args[3] = capabilities_string;
497 SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_START_COMMIT, hook, args,
498 hooks_env, NULL, pool));
504 /* Set *HANDLE to an open filehandle for a temporary file (i.e.,
505 automatically deleted when closed), into which the LOCK_TOKENS have
506 been written out in the format described in the pre-commit hook
509 LOCK_TOKENS is as returned by svn_fs__access_get_lock_tokens().
511 Allocate *HANDLE in POOL, and use POOL for temporary allocations. */
513 lock_token_content(apr_file_t **handle, apr_hash_t *lock_tokens,
516 svn_stringbuf_t *lock_str = svn_stringbuf_create("LOCK-TOKENS:\n", pool);
517 apr_hash_index_t *hi;
519 for (hi = apr_hash_first(pool, lock_tokens); hi;
520 hi = apr_hash_next(hi))
522 const char *token = apr_hash_this_key(hi);
523 const char *path = apr_hash_this_val(hi);
525 if (path == (const char *) 1)
527 /* Special handling for svn_fs_access_t * created by using deprecated
528 svn_fs_access_add_lock_token() function. */
533 path = svn_path_uri_autoescape(path, pool);
536 svn_stringbuf_appendstr(lock_str,
537 svn_stringbuf_createf(pool, "%s|%s\n", path, token));
540 svn_stringbuf_appendcstr(lock_str, "\n");
541 return create_temp_file(handle,
542 svn_stringbuf__morph_into_string(lock_str), pool);
548 svn_repos__hooks_pre_commit(svn_repos_t *repos,
549 apr_hash_t *hooks_env,
550 const char *txn_name,
553 const char *hook = svn_repos_pre_commit_hook(repos, pool);
554 svn_boolean_t broken_link;
556 if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link)
558 return hook_symlink_error(hook);
563 svn_fs_access_t *access_ctx;
564 apr_file_t *stdin_handle = NULL;
567 args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool);
571 SVN_ERR(svn_fs_get_access(&access_ctx, repos->fs));
574 apr_hash_t *lock_tokens = svn_fs__access_get_lock_tokens(access_ctx);
575 if (apr_hash_count(lock_tokens)) {
576 SVN_ERR(lock_token_content(&stdin_handle, lock_tokens, pool));
581 SVN_ERR(svn_io_file_open(&stdin_handle, SVN_NULL_DEVICE_NAME,
582 APR_READ, APR_OS_DEFAULT, pool));
584 SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_PRE_COMMIT, hook, args,
585 hooks_env, stdin_handle, pool));
593 svn_repos__hooks_post_commit(svn_repos_t *repos,
594 apr_hash_t *hooks_env,
596 const char *txn_name,
599 const char *hook = svn_repos_post_commit_hook(repos, pool);
600 svn_boolean_t broken_link;
602 if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link)
604 return hook_symlink_error(hook);
611 args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool);
612 args[2] = apr_psprintf(pool, "%ld", rev);
616 SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_POST_COMMIT, hook, args,
617 hooks_env, NULL, pool));
625 svn_repos__hooks_pre_revprop_change(svn_repos_t *repos,
626 apr_hash_t *hooks_env,
630 const svn_string_t *new_value,
634 const char *hook = svn_repos_pre_revprop_change_hook(repos, pool);
635 svn_boolean_t broken_link;
637 if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link)
639 return hook_symlink_error(hook);
644 apr_file_t *stdin_handle = NULL;
645 char action_string[2];
647 /* Pass the new value as stdin to hook */
649 SVN_ERR(create_temp_file(&stdin_handle, new_value, pool));
651 SVN_ERR(svn_io_file_open(&stdin_handle, SVN_NULL_DEVICE_NAME,
652 APR_READ, APR_OS_DEFAULT, pool));
654 action_string[0] = action;
655 action_string[1] = '\0';
658 args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool);
659 args[2] = apr_psprintf(pool, "%ld", rev);
660 args[3] = author ? author : "";
662 args[5] = action_string;
665 SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_PRE_REVPROP_CHANGE, hook,
666 args, hooks_env, stdin_handle, pool));
668 SVN_ERR(svn_io_file_close(stdin_handle, pool));
672 /* If the pre- hook doesn't exist at all, then default to
673 MASSIVE PARANOIA. Changing revision properties is a lossy
674 operation; so unless the repository admininstrator has
675 *deliberately* created the pre-hook, disallow all changes. */
678 (SVN_ERR_REPOS_DISABLED_FEATURE, NULL,
679 _("Repository has not been enabled to accept revision propchanges;\n"
680 "ask the administrator to create a pre-revprop-change hook"));
688 svn_repos__hooks_post_revprop_change(svn_repos_t *repos,
689 apr_hash_t *hooks_env,
693 const svn_string_t *old_value,
697 const char *hook = svn_repos_post_revprop_change_hook(repos, pool);
698 svn_boolean_t broken_link;
700 if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link)
702 return hook_symlink_error(hook);
707 apr_file_t *stdin_handle = NULL;
708 char action_string[2];
710 /* Pass the old value as stdin to hook */
712 SVN_ERR(create_temp_file(&stdin_handle, old_value, pool));
714 SVN_ERR(svn_io_file_open(&stdin_handle, SVN_NULL_DEVICE_NAME,
715 APR_READ, APR_OS_DEFAULT, pool));
717 action_string[0] = action;
718 action_string[1] = '\0';
721 args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool);
722 args[2] = apr_psprintf(pool, "%ld", rev);
723 args[3] = author ? author : "";
725 args[5] = action_string;
728 SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_POST_REVPROP_CHANGE, hook,
729 args, hooks_env, stdin_handle, pool));
731 SVN_ERR(svn_io_file_close(stdin_handle, pool));
739 svn_repos__hooks_pre_lock(svn_repos_t *repos,
740 apr_hash_t *hooks_env,
743 const char *username,
745 svn_boolean_t steal_lock,
748 const char *hook = svn_repos_pre_lock_hook(repos, pool);
749 svn_boolean_t broken_link;
751 if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link)
753 return hook_symlink_error(hook);
762 args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool);
765 args[4] = comment ? comment : "";
766 args[5] = steal_lock ? "1" : "0";
769 SVN_ERR(run_hook_cmd(&buf, SVN_REPOS__HOOK_PRE_LOCK, hook, args,
770 hooks_env, NULL, pool));
773 /* No validation here; the FS will take care of that. */
785 svn_repos__hooks_post_lock(svn_repos_t *repos,
786 apr_hash_t *hooks_env,
787 const apr_array_header_t *paths,
788 const char *username,
791 const char *hook = svn_repos_post_lock_hook(repos, pool);
792 svn_boolean_t broken_link;
794 if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link)
796 return hook_symlink_error(hook);
801 apr_file_t *stdin_handle = NULL;
802 svn_string_t *paths_str = svn_string_create(svn_cstring_join
806 SVN_ERR(create_temp_file(&stdin_handle, paths_str, pool));
809 args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool);
814 SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_POST_LOCK, hook, args,
815 hooks_env, stdin_handle, pool));
817 SVN_ERR(svn_io_file_close(stdin_handle, pool));
825 svn_repos__hooks_pre_unlock(svn_repos_t *repos,
826 apr_hash_t *hooks_env,
828 const char *username,
830 svn_boolean_t break_lock,
833 const char *hook = svn_repos_pre_unlock_hook(repos, pool);
834 svn_boolean_t broken_link;
836 if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link)
838 return hook_symlink_error(hook);
845 args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool);
847 args[3] = username ? username : "";
848 args[4] = token ? token : "";
849 args[5] = break_lock ? "1" : "0";
852 SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_PRE_UNLOCK, hook, args,
853 hooks_env, NULL, pool));
861 svn_repos__hooks_post_unlock(svn_repos_t *repos,
862 apr_hash_t *hooks_env,
863 const apr_array_header_t *paths,
864 const char *username,
867 const char *hook = svn_repos_post_unlock_hook(repos, pool);
868 svn_boolean_t broken_link;
870 if ((hook = check_hook_cmd(hook, &broken_link, pool)) && broken_link)
872 return hook_symlink_error(hook);
877 apr_file_t *stdin_handle = NULL;
878 svn_string_t *paths_str = svn_string_create(svn_cstring_join
882 SVN_ERR(create_temp_file(&stdin_handle, paths_str, pool));
885 args[1] = svn_dirent_local_style(svn_repos_path(repos, pool), pool);
886 args[2] = username ? username : "";
890 SVN_ERR(run_hook_cmd(NULL, SVN_REPOS__HOOK_POST_UNLOCK, hook, args,
891 hooks_env, stdin_handle, pool));
893 SVN_ERR(svn_io_file_close(stdin_handle, pool));
902 * vim:ts=4:sw=4:expandtab:tw=80:fo=tcroq
903 * vim:isk=a-z,A-Z,48-57,_,.,-,>
904 * vim:cino=>1s,e0,n0,f0,{.5s,}0,^-.5s,=.5s,t0,+1s,c3,(0,u0,\:0