From e96a82024013b3fbefa5b223be22f7e46aabfb3b Mon Sep 17 00:00:00 2001 From: bright-tools Date: Sun, 6 Apr 2014 19:02:57 +0100 Subject: [PATCH 01/36] API for retrieving details of the bugs referenced in commit comments --- Source/lang/strings_english.txt | 1 + Source/pages/issue_query.php | 37 +++++++++++++++++++++++++++++++++ Source/pages/si-common.php | 8 +++++++ 3 files changed, 46 insertions(+) create mode 100755 Source/pages/issue_query.php create mode 100755 Source/pages/si-common.php diff --git a/Source/lang/strings_english.txt b/Source/lang/strings_english.txt index f80bfc82d..c2606be69 100644 --- a/Source/lang/strings_english.txt +++ b/Source/lang/strings_english.txt @@ -139,6 +139,7 @@ $s_plugin_Source_invalid_checkin_url = 'Invalid remote check-in address'; $s_plugin_Source_invalid_import_url = 'Invalid remote import address'; $s_plugin_Source_invalid_repo = 'Invalid repository name'; $s_plugin_Source_invalid_changeset = 'Changeset information could not be loaded'; +$s_plugin_Source_invalid_key = 'Invalid API Key'; $s_plugin_Source_import_latest_failed = 'Repository latest data importing failed.'; $s_plugin_Source_import_full_failed = 'Full repository data importing failed.'; diff --git a/Source/pages/issue_query.php b/Source/pages/issue_query.php new file mode 100755 index 000000000..5c7bc727e --- /dev/null +++ b/Source/pages/issue_query.php @@ -0,0 +1,37 @@ +project_id ) ); + printf("Issue-%s-User: '%s'\r\n",$t_bug_id_str,user_get_name( $t_bug->handler_id ) ); + printf("Issue-%s-Resolved: '%s'\r\n",$t_bug_id_str,$t_bug->status < $t_resolved_threshold ); + } +} + +?> diff --git a/Source/pages/si-common.php b/Source/pages/si-common.php new file mode 100755 index 000000000..bc79f8da3 --- /dev/null +++ b/Source/pages/si-common.php @@ -0,0 +1,8 @@ + From c46bcf19462fd6ba682087d8dbadabd12a814895 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Sun, 6 Apr 2014 20:44:36 +0100 Subject: [PATCH 02/36] Add count of number of issues included in the output --- Source/pages/issue_query.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/pages/issue_query.php b/Source/pages/issue_query.php index 5c7bc727e..0edffb874 100755 --- a/Source/pages/issue_query.php +++ b/Source/pages/issue_query.php @@ -18,6 +18,7 @@ # Get a list of the bug IDs which were referenced in the commit comment $t_bug_list = Source_Parse_Buglinks( gpc_get_string( 'commit_comment', '' )); $t_resolved_threshold = config_get('bug_resolved_status_threshold'); +$t_bug_count = 0; foreach( $t_bug_list as $t_bug_id ) { @@ -31,7 +32,9 @@ printf("Issue-%s-Project: '%s'\r\n",$t_bug_id_str,project_get_name( $t_bug->project_id ) ); printf("Issue-%s-User: '%s'\r\n",$t_bug_id_str,user_get_name( $t_bug->handler_id ) ); printf("Issue-%s-Resolved: '%s'\r\n",$t_bug_id_str,$t_bug->status < $t_resolved_threshold ); + $t_bug_count++; } } +printf("Issue-Count: %s\r\n",$t_bug_count ); ?> From c7229ca3bd6d21cdf63ef0db999b36aaf1364d8c Mon Sep 17 00:00:00 2001 From: bright-tools Date: Mon, 7 Apr 2014 22:40:37 +0100 Subject: [PATCH 03/36] Output additional information relating to the bugs --- Source/pages/issue_query.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Source/pages/issue_query.php b/Source/pages/issue_query.php index 0edffb874..dc39683e3 100755 --- a/Source/pages/issue_query.php +++ b/Source/pages/issue_query.php @@ -22,17 +22,24 @@ foreach( $t_bug_list as $t_bug_id ) { + $t_bug_count++; + $t_bug_id_str = sprintf( "%08d", $t_bug_count ); + + printf("Issue-%s-ID: %d\r\n",$t_bug_id_str, $t_bug_id ); + # Check existence first to prevent API throwing an error if( bug_exists( $t_bug_id ) ) { $t_bug = bug_get( $t_bug_id ); - $t_bug_id_str = sprintf( "%08d", $t_bug_id ); - + printf("Issue-%s-Exists: 1\r\n",$t_bug_id_str ); printf("Issue-%s-Project: '%s'\r\n",$t_bug_id_str,project_get_name( $t_bug->project_id ) ); printf("Issue-%s-User: '%s'\r\n",$t_bug_id_str,user_get_name( $t_bug->handler_id ) ); printf("Issue-%s-Resolved: '%s'\r\n",$t_bug_id_str,$t_bug->status < $t_resolved_threshold ); - $t_bug_count++; + } + else + { + printf("Issue-%s-Exists: 0\r\n",$t_bug_id_str ); } } printf("Issue-Count: %s\r\n",$t_bug_count ); From a2dca996a9d0715bb6de384fcd2c4dd13f1aa704 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Mon, 7 Apr 2014 22:40:56 +0100 Subject: [PATCH 04/36] Initial hook implementation to check the committer and project (against the user the ticket is assigned to and the Mantis project it is within) --- SourceSVN/pre-commit.tmpl | 87 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100755 SourceSVN/pre-commit.tmpl diff --git a/SourceSVN/pre-commit.tmpl b/SourceSVN/pre-commit.tmpl new file mode 100755 index 000000000..48f48a094 --- /dev/null +++ b/SourceSVN/pre-commit.tmpl @@ -0,0 +1,87 @@ +#!/bin/sh + +# Copyright (c) 2014 John Bailey +# Licensed under the MIT license +# Requires GNU grep with PCRE + +URL="http://localhost/mantis/plugin.php?page=Source/issue_query" +API_KEY="dd" +SVNLOOK=/usr/bin/svnlook +CURL=/usr/bin/curl + +MY_PROJECT="Tep" + +REPOS="$1" +TXN="$2" +LOG_FILE=`mktemp /tmp/svn_log.XXX` +COMMENT_FILE=`mktemp /tmp/svn_comment.XXX` + +# Exit as soon as an error is encountered +#set -e + + +check_commit() +{ + COMMITTER=$1 + PROJECT=$2 + TICKET_OWNER=$3 + # TODO: Check the ticket's status + + echo "Checking $COMMITTER:$PROJECT:$TICKET_OWNER" 1>&2 + # You will probably need to modify the checks below depending on your requirements + if [ "x$MY_PROJECT" != "x$PROJECT" ]; then + echo "Ticket belongs to project '$PROJECT'. Was expecting this to be '$MY_PROJECT'" 1>&2 + exit 1 + fi; + if [ "x$COMMITTER" != "x$TICKET_OWNER" ]; then + echo "Ticket is assigned to '$TICKET_OWNER'. You're attempting to commit as '$COMMITTER'" 1>&2 + exit 1 + fi; +} + +if [ ! -x "$SVNLOOK" ]; then + echo "You need to update the script at $0 to point to svnlook" 1>&2 + exit 1 +fi + +if [ ! -x "$CURL" ]; then + echo "You need to update the script at $0 to point to curl" 1>&2 + exit 1 +fi + +echo 'commit_comment="' >> ${COMMENT_FILE} +"${SVNLOOK}" log -t "${TXN}" "${REPOS}" >> ${COMMENT_FILE} +echo '"' >> ${COMMENT_FILE} +"${CURL}" -d @${COMMENT_FILE} -d "api_key=${API_KEY}" ${URL} >> ${LOG_FILE} +COMMITTING_USER=$("${SVNLOOK}" author -t "${TXN}" "${REPOS}") + +COUNT=$(grep -oP 'Issue-Count: \K([0-9]+)' ${LOG_FILE}) + +if [ "x$COUNT" = "x" ]; then + echo "Didn't get a valid response from Mantis SourceIntegration" 1>&2 + exit 1 +elif [ $COUNT -eq 0 ]; then + echo "You need to reference at least one issue in your commit comment" 1>&2 + exit 1 +else + i=0 + for i in $(seq 1 $COUNT) + do + NUM=$(printf "%08d" $i) + ID=$(grep -oP "Issue-$NUM-ID: \K([a-zA-Z0-9_]+)" ${LOG_FILE}) + EXISTS=$(grep -oP "Issue-$NUM-Exists: \K([a-zA-Z0-9_]+)" ${LOG_FILE}) + + if [ $EXISTS -eq 1 ]; then + PROJECT=$(grep -oP "Issue-$NUM-Project: '\K(.+)(?=')" ${LOG_FILE}) + USER=$(grep -oP "Issue-$NUM-User: '\K([a-zA-Z0-9_.+-]+)(?=')" ${LOG_FILE}) + # TODO: Sanity check all the parameters before calling check_commit + check_commit "$COMMITTING_USER" "$PROJECT" "$USER" + else + echo "Issue ID $ID doesn't seem to exist in Mantis (exists:'$EXISTS')" 1>&2 + exit 1 + fi + done +fi + +exit 1 + From ba0aebfd41f0f6c0fe11368842d4b273465b84fe Mon Sep 17 00:00:00 2001 From: bright-tools Date: Wed, 9 Apr 2014 00:17:07 +0100 Subject: [PATCH 05/36] Start impementing second way to check commits, where Mantis does the checking and just reports status back to the hook --- Source/lang/strings_english.txt | 3 + Source/pages/pre_commit_check.php | 67 ++++++++++++++ Source/pages/repo_update.php | 2 + Source/pages/repo_update_page.php | 6 ++ .../pre-commit.tmpl.mantis-checks-commit | 56 ++++++++++++ SourceSVN/pre-commit.tmpl.repo-checks-commit | 89 +++++++++++++++++++ 6 files changed, 223 insertions(+) create mode 100755 Source/pages/pre_commit_check.php create mode 100755 SourceSVN/pre-commit.tmpl.mantis-checks-commit create mode 100755 SourceSVN/pre-commit.tmpl.repo-checks-commit diff --git a/Source/lang/strings_english.txt b/Source/lang/strings_english.txt index c2606be69..b5cacd36e 100644 --- a/Source/lang/strings_english.txt +++ b/Source/lang/strings_english.txt @@ -25,6 +25,7 @@ $s_plugin_Source_username = 'Username'; $s_plugin_Source_timestamp = 'Timestamp'; $s_plugin_Source_parent = 'Parent'; $s_plugin_Source_url = 'URL'; +$s_plugin_Source_commit_needs_issue = 'Commit Requires Issue Reference(s)'; $s_plugin_Source_info = 'Extra Info'; $s_plugin_Source_revision = 'Revision'; $s_plugin_Source_date_begin = 'Beginning Date'; @@ -144,5 +145,7 @@ $s_plugin_Source_invalid_key = 'Invalid API Key'; $s_plugin_Source_import_latest_failed = 'Repository latest data importing failed.'; $s_plugin_Source_import_full_failed = 'Full repository data importing failed.'; +$s_plugin_Source_error_commit_needs_issue = 'Commit comments needs to reference one or more issues'; + $s_plugin_Source_changeset_column_title = 'C'; diff --git a/Source/pages/pre_commit_check.php b/Source/pages/pre_commit_check.php new file mode 100755 index 000000000..8348c5744 --- /dev/null +++ b/Source/pages/pre_commit_check.php @@ -0,0 +1,67 @@ +info['repo_commit_needs_issue'] ) ? $t_repo->info['repo_commit_needs_issue'] : false; + +$t_all_ok = true; + + +if(( sizeof( $t_bug_list ) == 0 ) && $t_repo_commit_needs_issue ) +{ + printf("Check-Message: '%s'\r\n",plugin_lang_get( 'error_commit_needs_issue' ) ); + $t_all_ok = false; +} +else +{ + +foreach( $t_bug_list as $t_bug_id ) +{ + $t_bug_count++; + $t_bug_id_str = sprintf( "%08d", $t_bug_count ); + + printf("Issue-%s-ID: %d\r\n",$t_bug_id_str, $t_bug_id ); + + # Check existence first to prevent API throwing an error + if( bug_exists( $t_bug_id ) ) + { + $t_bug = bug_get( $t_bug_id ); + + printf("Issue-%s-Exists: 1\r\n",$t_bug_id_str ); + printf("Issue-%s-Project: '%s'\r\n",$t_bug_id_str,project_get_name( $t_bug->project_id ) ); + printf("Issue-%s-User: '%s'\r\n",$t_bug_id_str,user_get_name( $t_bug->handler_id ) ); + printf("Issue-%s-Resolved: '%s'\r\n",$t_bug_id_str,$t_bug->status < $t_resolved_threshold ); + } + else + { + printf("Issue-%s-Exists: 0\r\n",$t_bug_id_str ); + } +} +} +printf("Check-OK: %d\r\n",$t_all_ok ); + +?> diff --git a/Source/pages/repo_update.php b/Source/pages/repo_update.php index 99931f70b..4aa6c61c0 100644 --- a/Source/pages/repo_update.php +++ b/Source/pages/repo_update.php @@ -9,6 +9,7 @@ $f_repo_id = gpc_get_int( 'repo_id' ); $f_repo_name = gpc_get_string( 'repo_name' ); $f_repo_url = gpc_get_string( 'repo_url' ); +$f_repo_commit_needs_issue = gpc_get_bool( 'repo_commit_needs_issue', false ); $t_repo = SourceRepo::load( $f_repo_id ); $t_vcs = SourceVCS::repo( $t_repo ); @@ -16,6 +17,7 @@ $t_repo->name = $f_repo_name; $t_repo->url = $f_repo_url; +$t_repo->info['repo_commit_needs_issue'] = $f_repo_commit_needs_issue; $t_updated_repo = $t_vcs->update_repo( $t_repo ); diff --git a/Source/pages/repo_update_page.php b/Source/pages/repo_update_page.php index 881c421e5..511a19dcd 100644 --- a/Source/pages/repo_update_page.php +++ b/Source/pages/repo_update_page.php @@ -10,6 +10,7 @@ $t_repo = SourceRepo::load( $f_repo_id ); $t_vcs = SourceVCS::repo( $t_repo ); $t_type = SourceType($t_repo->type); +$t_repo_commit_needs_issue = isset( $t_repo->info['repo_commit_needs_issue'] ) ? $t_repo->info['repo_commit_needs_issue'] : ''; html_page_top1( plugin_lang_get( 'title' ) ); html_page_top2(); @@ -41,6 +42,11 @@ +> + +/> + + update_repo_form( $t_repo ) ?> diff --git a/SourceSVN/pre-commit.tmpl.mantis-checks-commit b/SourceSVN/pre-commit.tmpl.mantis-checks-commit new file mode 100755 index 000000000..632f5b74e --- /dev/null +++ b/SourceSVN/pre-commit.tmpl.mantis-checks-commit @@ -0,0 +1,56 @@ +#!/bin/sh + +# Copyright (c) 2014 John Bailey +# Licensed under the MIT license +# Requires GNU grep with PCRE + +URL="http://localhost/mantis/plugin.php?page=Source/pre_commit_check" +PROJECT="RepoName" +API_KEY="dd" +SVNLOOK=/usr/bin/svnlook +CURL=/usr/bin/curl + +REPOS="$1" +TXN="$2" +LOG_FILE=`mktemp /tmp/svn_log.XXX` +COMMENT_FILE=`mktemp /tmp/svn_comment.XXX` + +# Exit as soon as an error is encountered +#set -e + +if [ ! -x "$SVNLOOK" ]; then + echo "You need to update the script at $0 to point to svnlook" 1>&2 + exit 1 +fi + +if [ ! -x "$CURL" ]; then + echo "You need to update the script at $0 to point to curl" 1>&2 + exit 1 +fi + +COMMITTING_USER=$("${SVNLOOK}" author -t "${TXN}" "${REPOS}") +echo 'commit_comment="' >> ${COMMENT_FILE} +"${SVNLOOK}" log -t "${TXN}" "${REPOS}" >> ${COMMENT_FILE} +echo '"' >> ${COMMENT_FILE} +"${CURL}" -d "repo_name=${PROJECT}" -d "committer=${COMMITTING_USER}" -d @${COMMENT_FILE} -d "api_key=${API_KEY}" ${URL} >> ${LOG_FILE} + +RESULT=$(grep -oP 'Check-OK: \K([0-9]+)' ${LOG_FILE}) + +if [ "x$RESULT" = "x" ]; then + echo "Didn't get a valid response from Mantis SourceIntegration" 1>&2 + exit 1; +elif [ $RESULT -eq 0 ]; then + CHECK_MESSAGE=$(grep -oP "Check-Message: '\K(.+)(?=')" ${LOG_FILE}) + if [ "x$CHECK_MESSAGE" = "x" ]; then + echo "Mantis SourceIntegration bounced the commit but didn't report why" 1>&2 + else + echo $CHECK_MESSAGE 1>&2 + fi + exit 1 +else + # TODO + exit 1 +fi + +exit 1 + diff --git a/SourceSVN/pre-commit.tmpl.repo-checks-commit b/SourceSVN/pre-commit.tmpl.repo-checks-commit new file mode 100755 index 000000000..e81d7d950 --- /dev/null +++ b/SourceSVN/pre-commit.tmpl.repo-checks-commit @@ -0,0 +1,89 @@ +#!/bin/sh + +# Copyright (c) 2014 John Bailey +# Licensed under the MIT license +# Requires GNU grep with PCRE + +URL="http://localhost/mantis/plugin.php?page=Source/issue_query" +API_KEY="dd" +SVNLOOK=/usr/bin/svnlook +CURL=/usr/bin/curl + +MY_PROJECT="Tep" + +REPOS="$1" +TXN="$2" +LOG_FILE=`mktemp /tmp/svn_log.XXX` +COMMENT_FILE=`mktemp /tmp/svn_comment.XXX` + +# Exit as soon as an error is encountered +#set -e + + +check_commit() +{ + COMMITTER=$1 + PROJECT=$2 + TICKET_OWNER=$3 + # TODO: Check the ticket's status + + echo "Checking $COMMITTER:$PROJECT:$TICKET_OWNER" 1>&2 + # You will probably need to modify the checks below depending on your requirements + + # TODO: Check how this works for sub-projects + if [ "x$MY_PROJECT" != "x$PROJECT" ]; then + echo "Ticket belongs to project '$PROJECT'. Was expecting this to be '$MY_PROJECT'" 1>&2 + exit 1 + fi; + if [ "x$COMMITTER" != "x$TICKET_OWNER" ]; then + echo "Ticket is assigned to '$TICKET_OWNER'. You're attempting to commit as '$COMMITTER'" 1>&2 + exit 1 + fi; +} + +if [ ! -x "$SVNLOOK" ]; then + echo "You need to update the script at $0 to point to svnlook" 1>&2 + exit 1 +fi + +if [ ! -x "$CURL" ]; then + echo "You need to update the script at $0 to point to curl" 1>&2 + exit 1 +fi + +echo 'commit_comment="' >> ${COMMENT_FILE} +"${SVNLOOK}" log -t "${TXN}" "${REPOS}" >> ${COMMENT_FILE} +echo '"' >> ${COMMENT_FILE} +"${CURL}" -d @${COMMENT_FILE} -d "api_key=${API_KEY}" ${URL} >> ${LOG_FILE} +COMMITTING_USER=$("${SVNLOOK}" author -t "${TXN}" "${REPOS}") + +COUNT=$(grep -oP 'Issue-Count: \K([0-9]+)' ${LOG_FILE}) + +if [ "x$COUNT" = "x" ]; then + echo "Didn't get a valid response from Mantis SourceIntegration" 1>&2 + exit 1 +elif [ $COUNT -eq 0 ]; then + echo "You need to reference at least one issue in your commit comment" 1>&2 + exit 1 +else + i=0 + for i in $(seq 1 $COUNT) + do + NUM=$(printf "%08d" $i) + ID=$(grep -oP "Issue-$NUM-ID: \K([a-zA-Z0-9_]+)" ${LOG_FILE}) + EXISTS=$(grep -oP "Issue-$NUM-Exists: \K([a-zA-Z0-9_]+)" ${LOG_FILE}) + + if [ $EXISTS -eq 1 ]; then + PROJECT=$(grep -oP "Issue-$NUM-Project: '\K(.+)(?=')" ${LOG_FILE}) + USER=$(grep -oP "Issue-$NUM-User: '\K([a-zA-Z0-9_.+-]+)(?=')" ${LOG_FILE}) + # TODO: Sanity check all the parameters before calling check_commit + check_commit "$COMMITTING_USER" "$PROJECT" "$USER" + else + echo "Issue ID $ID doesn't seem to exist in Mantis (exists:'$EXISTS')" 1>&2 + exit 1 + fi + done +fi + +exit 1 + From 1058f12f45258a41012215be828b89e1ecd15424 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Wed, 9 Apr 2014 00:19:50 +0100 Subject: [PATCH 06/36] File moved --- SourceSVN/pre-commit.tmpl | 87 --------------------------------------- 1 file changed, 87 deletions(-) delete mode 100755 SourceSVN/pre-commit.tmpl diff --git a/SourceSVN/pre-commit.tmpl b/SourceSVN/pre-commit.tmpl deleted file mode 100755 index 48f48a094..000000000 --- a/SourceSVN/pre-commit.tmpl +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/sh - -# Copyright (c) 2014 John Bailey -# Licensed under the MIT license -# Requires GNU grep with PCRE - -URL="http://localhost/mantis/plugin.php?page=Source/issue_query" -API_KEY="dd" -SVNLOOK=/usr/bin/svnlook -CURL=/usr/bin/curl - -MY_PROJECT="Tep" - -REPOS="$1" -TXN="$2" -LOG_FILE=`mktemp /tmp/svn_log.XXX` -COMMENT_FILE=`mktemp /tmp/svn_comment.XXX` - -# Exit as soon as an error is encountered -#set -e - - -check_commit() -{ - COMMITTER=$1 - PROJECT=$2 - TICKET_OWNER=$3 - # TODO: Check the ticket's status - - echo "Checking $COMMITTER:$PROJECT:$TICKET_OWNER" 1>&2 - # You will probably need to modify the checks below depending on your requirements - if [ "x$MY_PROJECT" != "x$PROJECT" ]; then - echo "Ticket belongs to project '$PROJECT'. Was expecting this to be '$MY_PROJECT'" 1>&2 - exit 1 - fi; - if [ "x$COMMITTER" != "x$TICKET_OWNER" ]; then - echo "Ticket is assigned to '$TICKET_OWNER'. You're attempting to commit as '$COMMITTER'" 1>&2 - exit 1 - fi; -} - -if [ ! -x "$SVNLOOK" ]; then - echo "You need to update the script at $0 to point to svnlook" 1>&2 - exit 1 -fi - -if [ ! -x "$CURL" ]; then - echo "You need to update the script at $0 to point to curl" 1>&2 - exit 1 -fi - -echo 'commit_comment="' >> ${COMMENT_FILE} -"${SVNLOOK}" log -t "${TXN}" "${REPOS}" >> ${COMMENT_FILE} -echo '"' >> ${COMMENT_FILE} -"${CURL}" -d @${COMMENT_FILE} -d "api_key=${API_KEY}" ${URL} >> ${LOG_FILE} -COMMITTING_USER=$("${SVNLOOK}" author -t "${TXN}" "${REPOS}") - -COUNT=$(grep -oP 'Issue-Count: \K([0-9]+)' ${LOG_FILE}) - -if [ "x$COUNT" = "x" ]; then - echo "Didn't get a valid response from Mantis SourceIntegration" 1>&2 - exit 1 -elif [ $COUNT -eq 0 ]; then - echo "You need to reference at least one issue in your commit comment" 1>&2 - exit 1 -else - i=0 - for i in $(seq 1 $COUNT) - do - NUM=$(printf "%08d" $i) - ID=$(grep -oP "Issue-$NUM-ID: \K([a-zA-Z0-9_]+)" ${LOG_FILE}) - EXISTS=$(grep -oP "Issue-$NUM-Exists: \K([a-zA-Z0-9_]+)" ${LOG_FILE}) - - if [ $EXISTS -eq 1 ]; then - PROJECT=$(grep -oP "Issue-$NUM-Project: '\K(.+)(?=')" ${LOG_FILE}) - USER=$(grep -oP "Issue-$NUM-User: '\K([a-zA-Z0-9_.+-]+)(?=')" ${LOG_FILE}) - # TODO: Sanity check all the parameters before calling check_commit - check_commit "$COMMITTING_USER" "$PROJECT" "$USER" - else - echo "Issue ID $ID doesn't seem to exist in Mantis (exists:'$EXISTS')" 1>&2 - exit 1 - fi - done -fi - -exit 1 - From 9ae800f986d456e474ec760c4bd80a0c02ccc1cb Mon Sep 17 00:00:00 2001 From: bright-tools Date: Wed, 9 Apr 2014 00:20:06 +0100 Subject: [PATCH 07/36] Commenting --- SourceSVN/pre-commit.tmpl.mantis-checks-commit | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/SourceSVN/pre-commit.tmpl.mantis-checks-commit b/SourceSVN/pre-commit.tmpl.mantis-checks-commit index 632f5b74e..227f1c32a 100755 --- a/SourceSVN/pre-commit.tmpl.mantis-checks-commit +++ b/SourceSVN/pre-commit.tmpl.mantis-checks-commit @@ -29,11 +29,16 @@ if [ ! -x "$CURL" ]; then fi COMMITTING_USER=$("${SVNLOOK}" author -t "${TXN}" "${REPOS}") + +# Put the commit comment into a file which we can pass to curl echo 'commit_comment="' >> ${COMMENT_FILE} "${SVNLOOK}" log -t "${TXN}" "${REPOS}" >> ${COMMENT_FILE} echo '"' >> ${COMMENT_FILE} + +# Fire off the query to Mantis "${CURL}" -d "repo_name=${PROJECT}" -d "committer=${COMMITTING_USER}" -d @${COMMENT_FILE} -d "api_key=${API_KEY}" ${URL} >> ${LOG_FILE} +# Try and extract the result from the response RESULT=$(grep -oP 'Check-OK: \K([0-9]+)' ${LOG_FILE}) if [ "x$RESULT" = "x" ]; then From 55ec2e29d446f2eb5fcee80818a990c175b3f6e0 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Wed, 9 Apr 2014 00:21:11 +0100 Subject: [PATCH 08/36] Add *~ --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index dd4951179..35841d393 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .DS_Store *.buildpath *.project +*~ From 061514d31a152cc4fa2cfecda6d047aa29461655 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Wed, 9 Apr 2014 00:37:54 +0100 Subject: [PATCH 09/36] Added checking for existence of referenced issues --- Source/lang/strings_english.txt | 2 ++ Source/pages/pre_commit_check.php | 36 +++++++++++++++---------------- Source/pages/repo_update.php | 2 ++ Source/pages/repo_update_page.php | 6 ++++++ 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/Source/lang/strings_english.txt b/Source/lang/strings_english.txt index b5cacd36e..fb497b92c 100644 --- a/Source/lang/strings_english.txt +++ b/Source/lang/strings_english.txt @@ -26,6 +26,7 @@ $s_plugin_Source_timestamp = 'Timestamp'; $s_plugin_Source_parent = 'Parent'; $s_plugin_Source_url = 'URL'; $s_plugin_Source_commit_needs_issue = 'Commit Requires Issue Reference(s)'; +$s_plugin_Source_commit_issues_must_exist = 'Referenced Issue(s) Must Exist'; $s_plugin_Source_info = 'Extra Info'; $s_plugin_Source_revision = 'Revision'; $s_plugin_Source_date_begin = 'Beginning Date'; @@ -146,6 +147,7 @@ $s_plugin_Source_import_latest_failed = 'Repository latest data importing failed $s_plugin_Source_import_full_failed = 'Full repository data importing failed.'; $s_plugin_Source_error_commit_needs_issue = 'Commit comments needs to reference one or more issues'; +$s_plugin_Source_error_commit_nonexistent_issue = 'Commit comment references non-existent issue'; $s_plugin_Source_changeset_column_title = 'C'; diff --git a/Source/pages/pre_commit_check.php b/Source/pages/pre_commit_check.php index 8348c5744..da3e426dd 100755 --- a/Source/pages/pre_commit_check.php +++ b/Source/pages/pre_commit_check.php @@ -27,6 +27,7 @@ die( plugin_lang_get( 'invalid_repo' ) ); } $t_repo_commit_needs_issue = isset( $t_repo->info['repo_commit_needs_issue'] ) ? $t_repo->info['repo_commit_needs_issue'] : false; +$t_repo_commit_issues_must_exist = isset( $t_repo->info['repo_commit_issues_must_exist'] ) ? $t_repo->info['repo_commit_issues_must_exist'] : false; $t_all_ok = true; @@ -39,28 +40,25 @@ else { -foreach( $t_bug_list as $t_bug_id ) -{ - $t_bug_count++; - $t_bug_id_str = sprintf( "%08d", $t_bug_count ); - - printf("Issue-%s-ID: %d\r\n",$t_bug_id_str, $t_bug_id ); - - # Check existence first to prevent API throwing an error - if( bug_exists( $t_bug_id ) ) + foreach( $t_bug_list as $t_bug_id ) { - $t_bug = bug_get( $t_bug_id ); + $t_bug_count++; - printf("Issue-%s-Exists: 1\r\n",$t_bug_id_str ); - printf("Issue-%s-Project: '%s'\r\n",$t_bug_id_str,project_get_name( $t_bug->project_id ) ); - printf("Issue-%s-User: '%s'\r\n",$t_bug_id_str,user_get_name( $t_bug->handler_id ) ); - printf("Issue-%s-Resolved: '%s'\r\n",$t_bug_id_str,$t_bug->status < $t_resolved_threshold ); + # Check existence first to prevent API throwing an error + if( bug_exists( $t_bug_id ) ) + { + $t_bug = bug_get( $t_bug_id ); + } + else + { + if( $t_repo_commit_issues_must_exist ) + { + printf("Check-Message: '%s : %d'\r\n",plugin_lang_get( 'error_commit_nonexistent_issue' ), $t_bug_id ); + $t_all_ok = false; + break; + } + } } - else - { - printf("Issue-%s-Exists: 0\r\n",$t_bug_id_str ); - } -} } printf("Check-OK: %d\r\n",$t_all_ok ); diff --git a/Source/pages/repo_update.php b/Source/pages/repo_update.php index 4aa6c61c0..6d7a2ee35 100644 --- a/Source/pages/repo_update.php +++ b/Source/pages/repo_update.php @@ -10,6 +10,7 @@ $f_repo_name = gpc_get_string( 'repo_name' ); $f_repo_url = gpc_get_string( 'repo_url' ); $f_repo_commit_needs_issue = gpc_get_bool( 'repo_commit_needs_issue', false ); +$f_repo_commit_issues_must_exist = gpc_get_bool( 'repo_commit_issues_must_exist', false ); $t_repo = SourceRepo::load( $f_repo_id ); $t_vcs = SourceVCS::repo( $t_repo ); @@ -18,6 +19,7 @@ $t_repo->name = $f_repo_name; $t_repo->url = $f_repo_url; $t_repo->info['repo_commit_needs_issue'] = $f_repo_commit_needs_issue; +$t_repo->info['repo_commit_issues_must_exist'] = $f_repo_commit_issues_must_exist; $t_updated_repo = $t_vcs->update_repo( $t_repo ); diff --git a/Source/pages/repo_update_page.php b/Source/pages/repo_update_page.php index 511a19dcd..99f807408 100644 --- a/Source/pages/repo_update_page.php +++ b/Source/pages/repo_update_page.php @@ -11,6 +11,7 @@ $t_vcs = SourceVCS::repo( $t_repo ); $t_type = SourceType($t_repo->type); $t_repo_commit_needs_issue = isset( $t_repo->info['repo_commit_needs_issue'] ) ? $t_repo->info['repo_commit_needs_issue'] : ''; +$t_repo_commit_issues_must_exist = isset( $t_repo->info['repo_commit_issues_must_exist'] ) ? $t_repo->info['repo_commit_issues_must_exist'] : ''; html_page_top1( plugin_lang_get( 'title' ) ); html_page_top2(); @@ -47,6 +48,11 @@ /> +> + +/> + + update_repo_form( $t_repo ) ?> From 098088a77f6f8068ff0bd7f5f653783244b3db25 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Wed, 9 Apr 2014 00:58:02 +0100 Subject: [PATCH 10/36] Check that user name of committer matches that of user which bug is assigned to --- Source/lang/strings_english.txt | 2 ++ Source/pages/pre_commit_check.php | 25 ++++++++++++++++++++----- Source/pages/repo_update.php | 2 ++ Source/pages/repo_update_page.php | 6 ++++++ 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/Source/lang/strings_english.txt b/Source/lang/strings_english.txt index fb497b92c..14c4be3c4 100644 --- a/Source/lang/strings_english.txt +++ b/Source/lang/strings_english.txt @@ -27,6 +27,7 @@ $s_plugin_Source_parent = 'Parent'; $s_plugin_Source_url = 'URL'; $s_plugin_Source_commit_needs_issue = 'Commit Requires Issue Reference(s)'; $s_plugin_Source_commit_issues_must_exist = 'Referenced Issue(s) Must Exist'; +$s_plugin_Source_commit_ownership_must_match = 'Referenced Issue(s) Must Be Owned By Committer'; $s_plugin_Source_info = 'Extra Info'; $s_plugin_Source_revision = 'Revision'; $s_plugin_Source_date_begin = 'Beginning Date'; @@ -148,6 +149,7 @@ $s_plugin_Source_import_full_failed = 'Full repository data importing failed.'; $s_plugin_Source_error_commit_needs_issue = 'Commit comments needs to reference one or more issues'; $s_plugin_Source_error_commit_nonexistent_issue = 'Commit comment references non-existent issue'; +$s_plugin_Source_error_commit_issue_ownership = 'Issues referenced in commit need to be assigned to the committer'; $s_plugin_Source_changeset_column_title = 'C'; diff --git a/Source/pages/pre_commit_check.php b/Source/pages/pre_commit_check.php index da3e426dd..057a1502c 100755 --- a/Source/pages/pre_commit_check.php +++ b/Source/pages/pre_commit_check.php @@ -20,6 +20,7 @@ $t_resolved_threshold = config_get('bug_resolved_status_threshold'); $t_bug_count = 0; +$f_committer_name = gpc_get_string( 'committer', '' ); $f_repo_name = gpc_get_string( 'repo_name', '' ); $t_repo = SourceRepo::load_by_name( $f_repo_name ); # Repo not found @@ -28,6 +29,7 @@ } $t_repo_commit_needs_issue = isset( $t_repo->info['repo_commit_needs_issue'] ) ? $t_repo->info['repo_commit_needs_issue'] : false; $t_repo_commit_issues_must_exist = isset( $t_repo->info['repo_commit_issues_must_exist'] ) ? $t_repo->info['repo_commit_issues_must_exist'] : false; +$t_repo_commit_ownership_must_match = isset( $t_repo->info['repo_commit_ownership_must_match'] ) ? $t_repo->info['repo_commit_ownership_must_match'] : false; $t_all_ok = true; @@ -48,15 +50,28 @@ if( bug_exists( $t_bug_id ) ) { $t_bug = bug_get( $t_bug_id ); + + if( $t_repo_commit_ownership_must_match ) + { + $t_user_name = user_get_name( $t_bug->handler_id ); + $t_user_email = user_get_email( $t_bug->handler_id ); + if(!( strlen( $f_committer_name ) && ( $t_user_name == $f_committer_name ))) + { + printf("Check-Message: '%s : %d (%s vs %s)'\r\n",plugin_lang_get( 'error_commit_issue_ownership' ), $t_bug_id, $t_user_name, $f_committer_name ); + $t_all_ok = false; + break; + } + } } else { - if( $t_repo_commit_issues_must_exist ) - { + /* If the issue doesn't exist, then the ownership can't match */ + if( $t_repo_commit_issues_must_exist || $t_repo_commit_ownership_must_match ) + { printf("Check-Message: '%s : %d'\r\n",plugin_lang_get( 'error_commit_nonexistent_issue' ), $t_bug_id ); - $t_all_ok = false; - break; - } + $t_all_ok = false; + break; + } } } } diff --git a/Source/pages/repo_update.php b/Source/pages/repo_update.php index 6d7a2ee35..ad33e9496 100644 --- a/Source/pages/repo_update.php +++ b/Source/pages/repo_update.php @@ -11,6 +11,7 @@ $f_repo_url = gpc_get_string( 'repo_url' ); $f_repo_commit_needs_issue = gpc_get_bool( 'repo_commit_needs_issue', false ); $f_repo_commit_issues_must_exist = gpc_get_bool( 'repo_commit_issues_must_exist', false ); +$f_repo_commit_ownership_must_match = gpc_get_bool( 'repo_commit_ownership_must_match', false ); $t_repo = SourceRepo::load( $f_repo_id ); $t_vcs = SourceVCS::repo( $t_repo ); @@ -20,6 +21,7 @@ $t_repo->url = $f_repo_url; $t_repo->info['repo_commit_needs_issue'] = $f_repo_commit_needs_issue; $t_repo->info['repo_commit_issues_must_exist'] = $f_repo_commit_issues_must_exist; +$t_repo->info['repo_commit_ownership_must_match'] = $f_repo_commit_ownership_must_match; $t_updated_repo = $t_vcs->update_repo( $t_repo ); diff --git a/Source/pages/repo_update_page.php b/Source/pages/repo_update_page.php index 99f807408..58a8a21eb 100644 --- a/Source/pages/repo_update_page.php +++ b/Source/pages/repo_update_page.php @@ -12,6 +12,7 @@ $t_type = SourceType($t_repo->type); $t_repo_commit_needs_issue = isset( $t_repo->info['repo_commit_needs_issue'] ) ? $t_repo->info['repo_commit_needs_issue'] : ''; $t_repo_commit_issues_must_exist = isset( $t_repo->info['repo_commit_issues_must_exist'] ) ? $t_repo->info['repo_commit_issues_must_exist'] : ''; +$t_repo_commit_ownership_must_match = isset( $t_repo->info['repo_commit_ownership_must_match'] ) ? $t_repo->info['repo_commit_ownership_must_match'] : ''; html_page_top1( plugin_lang_get( 'title' ) ); html_page_top2(); @@ -53,6 +54,11 @@ /> +> + +/> + + update_repo_form( $t_repo ) ?> From 185bf8263f2796744aa195a243c4be36c9061648 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Thu, 10 Apr 2014 22:05:23 +0100 Subject: [PATCH 11/36] Add support for checking of ticket status in pre-commit checks --- Source/lang/strings_english.txt | 5 ++++- Source/pages/pre_commit_check.php | 14 +++++++++++--- Source/pages/repo_update.php | 4 ++++ Source/pages/repo_update_page.php | 23 ++++++++++++++++++++++- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/Source/lang/strings_english.txt b/Source/lang/strings_english.txt index 14c4be3c4..31096d78e 100644 --- a/Source/lang/strings_english.txt +++ b/Source/lang/strings_english.txt @@ -25,9 +25,11 @@ $s_plugin_Source_username = 'Username'; $s_plugin_Source_timestamp = 'Timestamp'; $s_plugin_Source_parent = 'Parent'; $s_plugin_Source_url = 'URL'; +$s_plugin_Source_pre_commit_checks = 'Pre-Commit Checks'; $s_plugin_Source_commit_needs_issue = 'Commit Requires Issue Reference(s)'; $s_plugin_Source_commit_issues_must_exist = 'Referenced Issue(s) Must Exist'; $s_plugin_Source_commit_ownership_must_match = 'Referenced Issue(s) Must Be Owned By Committer'; +$s_plugin_Source_commit_status_restricted = 'Only allow commits when ticket is:'; $s_plugin_Source_info = 'Extra Info'; $s_plugin_Source_revision = 'Revision'; $s_plugin_Source_date_begin = 'Beginning Date'; @@ -149,7 +151,8 @@ $s_plugin_Source_import_full_failed = 'Full repository data importing failed.'; $s_plugin_Source_error_commit_needs_issue = 'Commit comments needs to reference one or more issues'; $s_plugin_Source_error_commit_nonexistent_issue = 'Commit comment references non-existent issue'; -$s_plugin_Source_error_commit_issue_ownership = 'Issues referenced in commit need to be assigned to the committer'; +$s_plugin_Source_error_commit_issue_ownership = 'Issue referenced in commit need to be assigned to the committer'; +$s_plugin_Source_error_commit_issue_wrong_status = 'Issue referenced in commit is not at correct status to be committed against'; $s_plugin_Source_changeset_column_title = 'C'; diff --git a/Source/pages/pre_commit_check.php b/Source/pages/pre_commit_check.php index 057a1502c..63574229e 100755 --- a/Source/pages/pre_commit_check.php +++ b/Source/pages/pre_commit_check.php @@ -30,6 +30,8 @@ $t_repo_commit_needs_issue = isset( $t_repo->info['repo_commit_needs_issue'] ) ? $t_repo->info['repo_commit_needs_issue'] : false; $t_repo_commit_issues_must_exist = isset( $t_repo->info['repo_commit_issues_must_exist'] ) ? $t_repo->info['repo_commit_issues_must_exist'] : false; $t_repo_commit_ownership_must_match = isset( $t_repo->info['repo_commit_ownership_must_match'] ) ? $t_repo->info['repo_commit_ownership_must_match'] : false; +$t_repo_commit_status_restricted = isset( $t_repo->info['repo_commit_status_restricted'] ) ? $t_repo->info['repo_commit_status_restricted'] : false; +$t_repo_commit_status_allowed = isset( $t_repo->info['repo_commit_status_allowed'] ) ? $t_repo->info['repo_commit_status_allowed'] : ''; $t_all_ok = true; @@ -59,18 +61,24 @@ { printf("Check-Message: '%s : %d (%s vs %s)'\r\n",plugin_lang_get( 'error_commit_issue_ownership' ), $t_bug_id, $t_user_name, $f_committer_name ); $t_all_ok = false; - break; + } + } + if( $t_repo_commit_status_restricted ) + { + if( !in_array( $t_bug->status, $t_repo_commit_status_allowed )) + { + printf("Check-Message: '%s : %d (%s)'\r\n", plugin_lang_get( 'error_commit_issue_wrong_status' ), $t_bug_id, get_enum_element( 'status', $t_bug->status )); + $t_all_ok = false; } } } else { /* If the issue doesn't exist, then the ownership can't match */ - if( $t_repo_commit_issues_must_exist || $t_repo_commit_ownership_must_match ) + if( $t_repo_commit_issues_must_exist || $t_repo_commit_ownership_must_match || $t_repo_commit_status_restricted ) { printf("Check-Message: '%s : %d'\r\n",plugin_lang_get( 'error_commit_nonexistent_issue' ), $t_bug_id ); $t_all_ok = false; - break; } } } diff --git a/Source/pages/repo_update.php b/Source/pages/repo_update.php index ad33e9496..ce6dc3e20 100644 --- a/Source/pages/repo_update.php +++ b/Source/pages/repo_update.php @@ -12,6 +12,8 @@ $f_repo_commit_needs_issue = gpc_get_bool( 'repo_commit_needs_issue', false ); $f_repo_commit_issues_must_exist = gpc_get_bool( 'repo_commit_issues_must_exist', false ); $f_repo_commit_ownership_must_match = gpc_get_bool( 'repo_commit_ownership_must_match', false ); +$f_repo_commit_status_restricted = gpc_get_bool( 'repo_commit_status_restricted', false ); +$f_repo_commit_status_allowed = gpc_get_int_array( 'repo_commit_status_allowed', Array() ); $t_repo = SourceRepo::load( $f_repo_id ); $t_vcs = SourceVCS::repo( $t_repo ); @@ -22,6 +24,8 @@ $t_repo->info['repo_commit_needs_issue'] = $f_repo_commit_needs_issue; $t_repo->info['repo_commit_issues_must_exist'] = $f_repo_commit_issues_must_exist; $t_repo->info['repo_commit_ownership_must_match'] = $f_repo_commit_ownership_must_match; +$t_repo->info['repo_commit_status_restricted'] = $f_repo_commit_status_restricted; +$t_repo->info['repo_commit_status_allowed'] = $f_repo_commit_status_allowed; $t_updated_repo = $t_vcs->update_repo( $t_repo ); diff --git a/Source/pages/repo_update_page.php b/Source/pages/repo_update_page.php index 58a8a21eb..d5634d02a 100644 --- a/Source/pages/repo_update_page.php +++ b/Source/pages/repo_update_page.php @@ -13,6 +13,8 @@ $t_repo_commit_needs_issue = isset( $t_repo->info['repo_commit_needs_issue'] ) ? $t_repo->info['repo_commit_needs_issue'] : ''; $t_repo_commit_issues_must_exist = isset( $t_repo->info['repo_commit_issues_must_exist'] ) ? $t_repo->info['repo_commit_issues_must_exist'] : ''; $t_repo_commit_ownership_must_match = isset( $t_repo->info['repo_commit_ownership_must_match'] ) ? $t_repo->info['repo_commit_ownership_must_match'] : ''; +$t_repo_commit_status_restricted = isset( $t_repo->info['repo_commit_status_restricted'] ) ? $t_repo->info['repo_commit_status_restricted'] : ''; +$t_repo_commit_status_allowed = isset( $t_repo->info['repo_commit_status_allowed'] ) ? $t_repo->info['repo_commit_status_allowed'] : ''; html_page_top1( plugin_lang_get( 'title' ) ); html_page_top2(); @@ -44,6 +46,14 @@ + + +update_repo_form( $t_repo ) ?> + +> + + +> @@ -59,11 +69,22 @@ -update_repo_form( $t_repo ) ?> +> + + + + +> + + + +
/>/>
/>
+ + From 5b39619641895bce45c3527923643bf095ed4afc Mon Sep 17 00:00:00 2001 From: bright-tools Date: Thu, 10 Apr 2014 22:10:33 +0100 Subject: [PATCH 12/36] Update bounce message to include the list of valid statuses --- Source/pages/pre_commit_check.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Source/pages/pre_commit_check.php b/Source/pages/pre_commit_check.php index 63574229e..0c92c42f4 100755 --- a/Source/pages/pre_commit_check.php +++ b/Source/pages/pre_commit_check.php @@ -67,7 +67,18 @@ { if( !in_array( $t_bug->status, $t_repo_commit_status_allowed )) { - printf("Check-Message: '%s : %d (%s)'\r\n", plugin_lang_get( 'error_commit_issue_wrong_status' ), $t_bug_id, get_enum_element( 'status', $t_bug->status )); + printf("Check-Message: '%s : %d (%s vs ", plugin_lang_get( 'error_commit_issue_wrong_status' ), $t_bug_id, get_enum_element( 'status', $t_bug->status )); + $t_first = true; + foreach( $t_repo_commit_status_allowed as $t_allowed_status ) + { + if( !$t_first ) + { + printf(","); + } + printf( get_enum_element( 'status', $t_allowed_status )); + $t_first = false; + } + printf(")'\r\n"); $t_all_ok = false; } } From 7aad394bc1e587c7636000a272850466a1727d5c Mon Sep 17 00:00:00 2001 From: bright-tools Date: Thu, 10 Apr 2014 23:41:46 +0100 Subject: [PATCH 13/36] Add support for restricting commit based on project in which referenced issue resides --- Source/lang/strings_english.txt | 6 ++++- Source/pages/pre_commit_check.php | 44 ++++++++++++++++++++++++------- Source/pages/repo_update.php | 4 +++ Source/pages/repo_update_page.php | 14 +++++++++- 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/Source/lang/strings_english.txt b/Source/lang/strings_english.txt index 31096d78e..4c58c7659 100644 --- a/Source/lang/strings_english.txt +++ b/Source/lang/strings_english.txt @@ -29,7 +29,10 @@ $s_plugin_Source_pre_commit_checks = 'Pre-Commit Checks'; $s_plugin_Source_commit_needs_issue = 'Commit Requires Issue Reference(s)'; $s_plugin_Source_commit_issues_must_exist = 'Referenced Issue(s) Must Exist'; $s_plugin_Source_commit_ownership_must_match = 'Referenced Issue(s) Must Be Owned By Committer'; -$s_plugin_Source_commit_status_restricted = 'Only allow commits when ticket is:'; +$s_plugin_Source_commit_status_restricted = 'Referenced Issue(s) must be at a particular status'; +$s_plugin_Source_commit_status_restricted_list = 'Only allow commits when ticket is:'; +$s_plugin_Source_commit_project_restricted = 'Referenced Issue(s) must be withing a particular project'; +$s_plugin_Source_commit_project_restricted_list = 'Only allow commits when ticket is within:'; $s_plugin_Source_info = 'Extra Info'; $s_plugin_Source_revision = 'Revision'; $s_plugin_Source_date_begin = 'Beginning Date'; @@ -153,6 +156,7 @@ $s_plugin_Source_error_commit_needs_issue = 'Commit comments needs to reference $s_plugin_Source_error_commit_nonexistent_issue = 'Commit comment references non-existent issue'; $s_plugin_Source_error_commit_issue_ownership = 'Issue referenced in commit need to be assigned to the committer'; $s_plugin_Source_error_commit_issue_wrong_status = 'Issue referenced in commit is not at correct status to be committed against'; +$s_plugin_Source_error_commit_issue_wrong_project = 'Issue referenced in commit is within appropriate project'; $s_plugin_Source_changeset_column_title = 'C'; diff --git a/Source/pages/pre_commit_check.php b/Source/pages/pre_commit_check.php index 0c92c42f4..dfe1c6075 100755 --- a/Source/pages/pre_commit_check.php +++ b/Source/pages/pre_commit_check.php @@ -32,6 +32,8 @@ $t_repo_commit_ownership_must_match = isset( $t_repo->info['repo_commit_ownership_must_match'] ) ? $t_repo->info['repo_commit_ownership_must_match'] : false; $t_repo_commit_status_restricted = isset( $t_repo->info['repo_commit_status_restricted'] ) ? $t_repo->info['repo_commit_status_restricted'] : false; $t_repo_commit_status_allowed = isset( $t_repo->info['repo_commit_status_allowed'] ) ? $t_repo->info['repo_commit_status_allowed'] : ''; +$t_repo_commit_project_restricted = isset( $t_repo->info['repo_commit_project_restricted'] ) ? $t_repo->info['repo_commit_project_restricted'] : ''; +$t_repo_commit_project_allowed = isset( $t_repo->info['repo_commit_project_allowed'] ) ? $t_repo->info['repo_commit_project_allowed'] : ''; $t_all_ok = true; @@ -68,16 +70,38 @@ if( !in_array( $t_bug->status, $t_repo_commit_status_allowed )) { printf("Check-Message: '%s : %d (%s vs ", plugin_lang_get( 'error_commit_issue_wrong_status' ), $t_bug_id, get_enum_element( 'status', $t_bug->status )); - $t_first = true; - foreach( $t_repo_commit_status_allowed as $t_allowed_status ) - { - if( !$t_first ) - { - printf(","); - } - printf( get_enum_element( 'status', $t_allowed_status )); - $t_first = false; - } + $t_first = true; + # Output the list of statuses for which commit is allowed + foreach( $t_repo_commit_status_allowed as $t_allowed_status ) + { + if( !$t_first ) + { + printf(", "); + } + printf( get_enum_element( 'status', $t_allowed_status )); + $t_first = false; + } + printf(")'\r\n"); + $t_all_ok = false; + } + } + if( 1|| $t_repo_commit_project_restricted ) + { + if( !in_array( 0, $t_repo_commit_project_allowed ) && + !in_array( $t_bug->project_id, $t_repo_commit_project_allowed )) + { + printf("Check-Message: '%s : %d (%s vs ", plugin_lang_get( 'error_commit_issue_wrong_project' ), $t_bug_id, project_get_field( $t_bug->project_id, 'name' )); + $t_first = true; + # Output the list of projects for which commit is allowed + foreach( $t_repo_commit_project_allowed as $t_allowed_project ) + { + if( !$t_first ) + { + printf(", "); + } + printf( project_get_field( $t_allowed_project, 'name' ) ); + $t_first = false; + } printf(")'\r\n"); $t_all_ok = false; } diff --git a/Source/pages/repo_update.php b/Source/pages/repo_update.php index ce6dc3e20..3d8884ef2 100644 --- a/Source/pages/repo_update.php +++ b/Source/pages/repo_update.php @@ -14,6 +14,8 @@ $f_repo_commit_ownership_must_match = gpc_get_bool( 'repo_commit_ownership_must_match', false ); $f_repo_commit_status_restricted = gpc_get_bool( 'repo_commit_status_restricted', false ); $f_repo_commit_status_allowed = gpc_get_int_array( 'repo_commit_status_allowed', Array() ); +$f_repo_commit_project_restricted = gpc_get_bool( 'repo_commit_project_restricted', false ); +$f_repo_commit_project_allowed = gpc_get_int_array( 'repo_commit_project_allowed', Array() ); $t_repo = SourceRepo::load( $f_repo_id ); $t_vcs = SourceVCS::repo( $t_repo ); @@ -26,6 +28,8 @@ $t_repo->info['repo_commit_ownership_must_match'] = $f_repo_commit_ownership_must_match; $t_repo->info['repo_commit_status_restricted'] = $f_repo_commit_status_restricted; $t_repo->info['repo_commit_status_allowed'] = $f_repo_commit_status_allowed; +$t_repo->info['repo_commit_project_restricted'] = $f_repo_commit_project_restricted; +$t_repo->info['repo_commit_project_allowed'] = $f_repo_commit_project_allowed; $t_updated_repo = $t_vcs->update_repo( $t_repo ); diff --git a/Source/pages/repo_update_page.php b/Source/pages/repo_update_page.php index d5634d02a..4f43c6677 100644 --- a/Source/pages/repo_update_page.php +++ b/Source/pages/repo_update_page.php @@ -15,6 +15,8 @@ $t_repo_commit_ownership_must_match = isset( $t_repo->info['repo_commit_ownership_must_match'] ) ? $t_repo->info['repo_commit_ownership_must_match'] : ''; $t_repo_commit_status_restricted = isset( $t_repo->info['repo_commit_status_restricted'] ) ? $t_repo->info['repo_commit_status_restricted'] : ''; $t_repo_commit_status_allowed = isset( $t_repo->info['repo_commit_status_allowed'] ) ? $t_repo->info['repo_commit_status_allowed'] : ''; +$t_repo_commit_project_restricted = isset( $t_repo->info['repo_commit_project_restricted'] ) ? $t_repo->info['repo_commit_project_restricted'] : ''; +$t_repo_commit_project_allowed = isset( $t_repo->info['repo_commit_project_allowed'] ) ? $t_repo->info['repo_commit_project_allowed'] : ''; html_page_top1( plugin_lang_get( 'title' ) ); html_page_top2(); @@ -75,10 +77,20 @@ > - + +> + +/> + + +> + + + + From 9d60cc6e506fa1c1cb4e670d80576ea33c815bc1 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Fri, 30 May 2014 20:36:41 +0100 Subject: [PATCH 14/36] Add documentation --- CommitMessageCheck.md | 109 +++++++++++++++++++++++++++++++++++ docimgs/configure_checks.png | Bin 0 -> 72614 bytes 2 files changed, 109 insertions(+) create mode 100755 CommitMessageCheck.md create mode 100755 docimgs/configure_checks.png diff --git a/CommitMessageCheck.md b/CommitMessageCheck.md new file mode 100755 index 000000000..89b41d974 --- /dev/null +++ b/CommitMessageCheck.md @@ -0,0 +1,109 @@ +## Introduction + +*What's the problem?* + +I've got a Mantis project which I use to manage the development of my project/product. When people commit to the version control repository they reference the Mantis issue number in the commits. +I want one or more of the following: + +1. Ensure that commit comments do actually reference one or more issues. I don't think that anyone should be making commits without referencing an issue. +1. I want to ensure that the issue(s) which are referenced in the commit comment are assigned to the person performing the commit +1. I want to ensure that the issue(s) which are referenced in the commit comment are at the confirmed or assigned state and haven't already been closed +1. I want to ensure that the issue(s) which are referenced in the commit comment belong to the appropriate project + +*OK, what's your solution then?* + +To build on the [Source +Integration](https://github.com/mantisbt-plugins/source-integration) plugin in +order to implement the checks and to utilise pre-commit hooks in order to make +the appropriate call-backs to Mantis. If the checks performed by the hook don't +pass then the commit is prevented by the VCS + +*I don't think that you should be so rule-happy with commit comments!* + +It's a matter of choice - some projects prefer to have a greater degree of control/enforcement than others. All of the checks are optional and none are enabled by default. + +*I don't like your software - how else could I do this?* + +I know of the following: + +- [RepoGuard](http://repoguard.tigris.org/) + +## How To Install + +### Step 1 : Install the Mantis Plugin. + +The code is a branch of the Source Integration plugin. Go [here](https://github.com/bright-tools/source-integration) and click the "Download ZIP" link in order to get it. + +Copy the contents of the zip file to the `plugins` directory in your Mantis install. + +Follow steps 4-10 of the [Source Integration plugin installation guide](https://github.com/mantisbt-plugins/source-integration/blob/master/README.md) + +### Step 2 : Install the Hook + +Several example hooks are provided - which one you need to use depends on which version control system you're using. + +#### Subversion (SVN) + +Copy the `SourceSVN/pre-commit.tmpl.mantis-checks-commit` file from the ZIP to your SVN repository's `hooks` directory. Ensure that the file ownership is appropriate it has executable permission set. More information on hooks can be found in the [Version Control with Subversion](http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.reposadmin.create.hooks) book. + +Rename the file within the `hooks` directory to be `pre-commit.tmpl` (i.e. remove the `.tmpl.mantis-checks-commit` extension) + +Modify the `URL` setting to point to the web interface to your Mantis installation. Don't remove the `plugin.php` part or the text which follows that. + +Modify the `PROJECT` setting to match the name that you gave the repository when configuring it in Mantis (Part of Step 1). + +Modify the `API_KEY` setting to match that which Mantis is configured to use (again, part of Step 1). + +If necessary, update the `SVNLOOK` and `CURL` settings to point to where those tools are installed. + +#### Git + +TODO + +### Step 3 : Configure the Mantis Plugin + +All that's left to do now is to choose which checks you want to associate with +the repository. For the purposes of this guide it is assumed that you already +have the basic repository details set up in Mantis as part of your +SourceIntegration configuration. If not then you will need to perform this step +first. + +In the Mantis web front-end, select the "Repositories" link, then click the +"Manage" link on the repository you wish to set up commit checking on. Select +"Update Repository" and you will see a list of configuration options. + +![Configuration options](docimgs/configure_checks.png) + +Select those that you want (see the table below) and click "Update Repository" + + +| Option | Description | +|------------------------------------|--------------| +| Commit Required Issue Reference(s) | When enabled, this check will ensure that the commit comment contains references to one or more Mantis bug IDs. The format of the reference is as per the SourceIntegration plugin's "Bug Link Regex" settings. | +| Referenced Issue(s) Must Exist | When enabled, this check will ensure that the Mantis bug IDs referenced in the commit comment are associated with tickets which exist in Mantis | +| Referenced Issue(s) Must Be Owned By Committer | When enabled, this check will cross-reference the user-name of the committer with the user-name to which the bug ID(s) referenced in the commit comment are assigned. In the case that they don't all match, the check will fail | +| Committer must be a member of the Mantis project | When enabled, this check will cross-reference the user-name of the committer and the user-names associated with the Mantis project (see the project's configuration under "Manage Projects" in Mantis). In the case that the committer is not listed as a project member, the check will fail | +| Committer Must Be A | In the case that the previous check is enabled, this field allows the check to be expanded to limit the access levels at which a commit is allowed. By default, commit is allowed at all levels. | +| Referenced Issue(s) must be at a particular status | When enabled, this check examine the status of the bugs referenced in the commit comment. In the case that one or more of the tickets is not one of the statuses listed in the "Only allow commit when ticket is" list, the check will fail | +| Only allow commit when ticket is | See previous item | +| Referenced Issue(s) must be within a particular project | When enabled, this check cross-references the Mantis projects to which referenced bugs belong and the list specified in the "Only allow commits when ticket is within" option. In the case that one or more ticket is not a member of the specified list of Mantis projects, the check fails. | +| Only allow commits when ticket is within | See previous item | + +### Step 4 : Quick Test (Optional) + +A simple way to quickly test that your set-up is working is to enable the option to enforce issue references in commit comments, then to attempt to commit to the repository without referencing any issues. The commit should be refused. Of course in the case that the installation has not worked correctly, you will actually commit the file, so please be prepared for this! + +## How to use + +If the installation has been successful then all you need to do is ensure that +commit comments reference the appropriate tickets, using the appropriate format +of comments. The format for referencing comments is configured as a regular +expression as part of the Source Integration plugin configuration. By default +it recognises comments containing text such as `issue #12` or `issue #12,#61`, etc. + +## Potential Problems + +*The user-names used for version control don't match those used in Mantis* + +That's unfortunate and will require some manual intervention. Possibly the easiest way to work around this is to create a look-up function to return the Mantis user-name based on the version control user name then modify the hook function to call this. + diff --git a/docimgs/configure_checks.png b/docimgs/configure_checks.png new file mode 100755 index 0000000000000000000000000000000000000000..d0a93d8e120838bd256404adf98aa3016ffa4cc3 GIT binary patch literal 72614 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 literal 0 HcmV?d00001 From 8d7a64dc99516f9d85a148e757b3f8eb903318a5 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Fri, 30 May 2014 21:42:34 +0100 Subject: [PATCH 15/36] Clean ups Add check that committer is member of mantis project U/I updates to disable/enable controls based on what options are set --- Source/lang/strings_english.txt | 7 +- Source/pages/pre_commit_check.php | 101 ++++++++++++++---- Source/pages/repo_update.php | 8 +- Source/pages/repo_update_page.php | 66 +++++++++--- .../pre-commit.tmpl.mantis-checks-commit | 8 ++ 5 files changed, 155 insertions(+), 35 deletions(-) mode change 100644 => 100755 Source/lang/strings_english.txt mode change 100644 => 100755 Source/pages/repo_update.php mode change 100644 => 100755 Source/pages/repo_update_page.php diff --git a/Source/lang/strings_english.txt b/Source/lang/strings_english.txt old mode 100644 new mode 100755 index 4c58c7659..bc7c76767 --- a/Source/lang/strings_english.txt +++ b/Source/lang/strings_english.txt @@ -29,9 +29,11 @@ $s_plugin_Source_pre_commit_checks = 'Pre-Commit Checks'; $s_plugin_Source_commit_needs_issue = 'Commit Requires Issue Reference(s)'; $s_plugin_Source_commit_issues_must_exist = 'Referenced Issue(s) Must Exist'; $s_plugin_Source_commit_ownership_must_match = 'Referenced Issue(s) Must Be Owned By Committer'; +$s_plugin_Source_commit_committer_must_be_member = 'Committer must be a member of the Mantis project'; +$s_plugin_Source_commit_committer_must_be_level = 'Committer must be a:'; $s_plugin_Source_commit_status_restricted = 'Referenced Issue(s) must be at a particular status'; $s_plugin_Source_commit_status_restricted_list = 'Only allow commits when ticket is:'; -$s_plugin_Source_commit_project_restricted = 'Referenced Issue(s) must be withing a particular project'; +$s_plugin_Source_commit_project_restricted = 'Referenced Issue(s) must be within a particular project'; $s_plugin_Source_commit_project_restricted_list = 'Only allow commits when ticket is within:'; $s_plugin_Source_info = 'Extra Info'; $s_plugin_Source_revision = 'Revision'; @@ -155,6 +157,9 @@ $s_plugin_Source_import_full_failed = 'Full repository data importing failed.'; $s_plugin_Source_error_commit_needs_issue = 'Commit comments needs to reference one or more issues'; $s_plugin_Source_error_commit_nonexistent_issue = 'Commit comment references non-existent issue'; $s_plugin_Source_error_commit_issue_ownership = 'Issue referenced in commit need to be assigned to the committer'; +$s_plugin_Source_error_commit_committer_not_member = 'Committing user is not a member of the Mantis project'; +$s_plugin_Source_error_commit_committer_not_fount = 'Committing user could not be found in Mantis'; +$s_plugin_Source_error_commit_committer_wrong_level = 'Committing user does not have appropriate access level in project'; $s_plugin_Source_error_commit_issue_wrong_status = 'Issue referenced in commit is not at correct status to be committed against'; $s_plugin_Source_error_commit_issue_wrong_project = 'Issue referenced in commit is within appropriate project'; diff --git a/Source/pages/pre_commit_check.php b/Source/pages/pre_commit_check.php index dfe1c6075..5410e6cc2 100755 --- a/Source/pages/pre_commit_check.php +++ b/Source/pages/pre_commit_check.php @@ -2,29 +2,28 @@ # Copyright (c) 2014 John Bailey # Licensed under the MIT license +# +# This file is intended to be called from a pre-commit hook in order to verify +# mantis ticket references in the commit comment. The level of checking +# is configured on a per-repository basis include_once 'si-common.php'; -$t_valid = false; - -if ( si_is_key_ok() ) { - $t_valid = true; -} - -if ( !$t_valid ) { +if (! si_is_key_ok() ) { +# TODO: Does this error make it back via the hook? die( plugin_lang_get( 'invalid_key' ) ); } # Get a list of the bug IDs which were referenced in the commit comment $t_bug_list = Source_Parse_Buglinks( gpc_get_string( 'commit_comment', '' )); $t_resolved_threshold = config_get('bug_resolved_status_threshold'); -$t_bug_count = 0; $f_committer_name = gpc_get_string( 'committer', '' ); $f_repo_name = gpc_get_string( 'repo_name', '' ); $t_repo = SourceRepo::load_by_name( $f_repo_name ); # Repo not found if ( is_null( $t_repo ) ) { +# TODO: Does this error make it back via the hook? die( plugin_lang_get( 'invalid_repo' ) ); } $t_repo_commit_needs_issue = isset( $t_repo->info['repo_commit_needs_issue'] ) ? $t_repo->info['repo_commit_needs_issue'] : false; @@ -34,22 +33,24 @@ $t_repo_commit_status_allowed = isset( $t_repo->info['repo_commit_status_allowed'] ) ? $t_repo->info['repo_commit_status_allowed'] : ''; $t_repo_commit_project_restricted = isset( $t_repo->info['repo_commit_project_restricted'] ) ? $t_repo->info['repo_commit_project_restricted'] : ''; $t_repo_commit_project_allowed = isset( $t_repo->info['repo_commit_project_allowed'] ) ? $t_repo->info['repo_commit_project_allowed'] : ''; +$t_repo_commit_committer_must_be_member = isset( $t_repo->info['repo_commit_committer_must_be_member'] ) ? $t_repo->info['repo_commit_committer_must_be_member'] : ''; +$t_repo_commit_committer_must_be_level = isset( $t_repo->info['repo_commit_committer_must_be_level'] ) ? $t_repo->info['repo_commit_committer_must_be_level'] : MantisEnum::getValues( config_get( 'access_levels_enum_string' ) ) ; $t_all_ok = true; - +# Check number of bugs referenced in the commit comment if(( sizeof( $t_bug_list ) == 0 ) && $t_repo_commit_needs_issue ) { + # It was expected that the commit comment would reference one of more bug + # IDs but this was not the case + printf("Check-Message: '%s'\r\n",plugin_lang_get( 'error_commit_needs_issue' ) ); $t_all_ok = false; } else { - foreach( $t_bug_list as $t_bug_id ) { - $t_bug_count++; - # Check existence first to prevent API throwing an error if( bug_exists( $t_bug_id ) ) { @@ -59,18 +60,28 @@ { $t_user_name = user_get_name( $t_bug->handler_id ); $t_user_email = user_get_email( $t_bug->handler_id ); + +# TODO: Check vs e-mail address + + # Check that the username of the committer matches the user name + # of the owner of the ticket if(!( strlen( $f_committer_name ) && ( $t_user_name == $f_committer_name ))) { - printf("Check-Message: '%s : %d (%s vs %s)'\r\n",plugin_lang_get( 'error_commit_issue_ownership' ), $t_bug_id, $t_user_name, $f_committer_name ); + printf("Check-Message: '%s : %d (%s vs %s)'\r\n", + plugin_lang_get( 'error_commit_issue_ownership' ), $t_bug_id, $t_user_name, $f_committer_name ); $t_all_ok = false; } } if( $t_repo_commit_status_restricted ) { + # Check that the bug's status is at a level for which a commit + # is allowed if( !in_array( $t_bug->status, $t_repo_commit_status_allowed )) { - printf("Check-Message: '%s : %d (%s vs ", plugin_lang_get( 'error_commit_issue_wrong_status' ), $t_bug_id, get_enum_element( 'status', $t_bug->status )); + printf("Check-Message: '%s : %d (%s vs ", + plugin_lang_get( 'error_commit_issue_wrong_status' ), $t_bug_id, get_enum_element( 'status', $t_bug->status )); $t_first = true; + # Output the list of statuses for which commit is allowed foreach( $t_repo_commit_status_allowed as $t_allowed_status ) { @@ -85,13 +96,15 @@ $t_all_ok = false; } } - if( 1|| $t_repo_commit_project_restricted ) + if( $t_repo_commit_project_restricted ) { - if( !in_array( 0, $t_repo_commit_project_allowed ) && + if( !in_array( 0, $t_repo_commit_project_allowed ) && !in_array( $t_bug->project_id, $t_repo_commit_project_allowed )) { - printf("Check-Message: '%s : %d (%s vs ", plugin_lang_get( 'error_commit_issue_wrong_project' ), $t_bug_id, project_get_field( $t_bug->project_id, 'name' )); + printf("Check-Message: '%s : %d (%s vs ", + plugin_lang_get( 'error_commit_issue_wrong_project' ), $t_bug_id, project_get_field( $t_bug->project_id, 'name' )); $t_first = true; + # Output the list of projects for which commit is allowed foreach( $t_repo_commit_project_allowed as $t_allowed_project ) { @@ -106,13 +119,61 @@ $t_all_ok = false; } } + if( $t_repo_commit_committer_must_be_member ) + { + $t_user_id = user_get_id_by_name( $f_committer_name ); + + /* Check that the user exists in Mantis */ + if( $t_user_id == false ) + { + printf("Check-Message: '%s : %d (%s)'\r\n", + plugin_lang_get( 'error_commit_committer_not_found' ), $t_bug_id, $f_committer_name ); + $t_all_ok = false; + } + /* Check that the user is assigned to the project */ + elseif( ! project_includes_user( $t_bug->project_id, $t_user_id )) + { + printf("Check-Message: '%s : %d (%s)'\r\n", + plugin_lang_get( 'error_commit_committer_not_member' ), $t_bug_id, $f_committer_name ); + $t_all_ok = false; + } + else + { + $t_user_access_level = project_get_local_user_access_level( $t_bug->project_id, $t_user_id ); + if( !in_array( $t_user_access_level, $t_repo_commit_committer_must_be_level )) + { + printf("Check-Message: '%s : %d (%s vs", + plugin_lang_get( 'error_commit_committer_wrong_level' ), $t_bug_id, $f_committer_name ); + $t_first = true; + $t_levels = MantisEnum::getValues( config_get( 'access_levels_enum_string' ) ); + # Output the list of projects for which commit is allowed + foreach( $t_repo_commit_committer_must_be_level as $t_allowed_level ) + { + if( !$t_first ) + { + printf(", "); + } + printf( $t_levels[ $t_allowed_level ] ); + $t_first = false; + } + + printf(")'\r\n"); + $t_all_ok = false; + } + } + } } else { - /* If the issue doesn't exist, then the ownership can't match */ - if( $t_repo_commit_issues_must_exist || $t_repo_commit_ownership_must_match || $t_repo_commit_status_restricted ) + /* If the issue doesn't exist, then can't perform the checks */ + if( $t_repo_commit_issues_must_exist || + $t_repo_commit_ownership_must_match || + $t_repo_commit_status_restricted || + $t_repo_commit_project_restricted || + $t_repo_commit_committer_must_be_member ) { - printf("Check-Message: '%s : %d'\r\n",plugin_lang_get( 'error_commit_nonexistent_issue' ), $t_bug_id ); + printf("Check-Message: '%s : %d'\r\n", + plugin_lang_get( 'error_commit_nonexistent_issue' ), $t_bug_id ); $t_all_ok = false; } } diff --git a/Source/pages/repo_update.php b/Source/pages/repo_update.php old mode 100644 new mode 100755 index 3d8884ef2..112d2f220 --- a/Source/pages/repo_update.php +++ b/Source/pages/repo_update.php @@ -13,9 +13,11 @@ $f_repo_commit_issues_must_exist = gpc_get_bool( 'repo_commit_issues_must_exist', false ); $f_repo_commit_ownership_must_match = gpc_get_bool( 'repo_commit_ownership_must_match', false ); $f_repo_commit_status_restricted = gpc_get_bool( 'repo_commit_status_restricted', false ); -$f_repo_commit_status_allowed = gpc_get_int_array( 'repo_commit_status_allowed', Array() ); +$f_repo_commit_status_allowed = gpc_get_int_array( 'repo_commit_status_allowed', MantisEnum::getValues( config_get( 'status_enum_string' ) )); $f_repo_commit_project_restricted = gpc_get_bool( 'repo_commit_project_restricted', false ); -$f_repo_commit_project_allowed = gpc_get_int_array( 'repo_commit_project_allowed', Array() ); +$f_repo_commit_project_allowed = gpc_get_int_array( 'repo_commit_project_allowed', Array( 0 ) ); +$f_repo_commit_committer_must_be_member = gpc_get_bool( 'repo_commit_committer_must_me_member', false ); +$f_repo_commit_committer_must_be_level = gpc_get_int_array( 'repo_commit_committer_must_be_level', MantisEnum::getValues( config_get( 'access_levels_enum_string' ) )); $t_repo = SourceRepo::load( $f_repo_id ); $t_vcs = SourceVCS::repo( $t_repo ); @@ -30,6 +32,8 @@ $t_repo->info['repo_commit_status_allowed'] = $f_repo_commit_status_allowed; $t_repo->info['repo_commit_project_restricted'] = $f_repo_commit_project_restricted; $t_repo->info['repo_commit_project_allowed'] = $f_repo_commit_project_allowed; +$t_repo->info['repo_commit_committer_must_be_member'] = $f_repo_commit_committer_must_be_member; +$t_repo->info['repo_commit_committer_must_be_level'] = $f_repo_commit_committer_must_be_level; $t_updated_repo = $t_vcs->update_repo( $t_repo ); diff --git a/Source/pages/repo_update_page.php b/Source/pages/repo_update_page.php old mode 100644 new mode 100755 index 4f43c6677..3da36a212 --- a/Source/pages/repo_update_page.php +++ b/Source/pages/repo_update_page.php @@ -10,16 +10,48 @@ $t_repo = SourceRepo::load( $f_repo_id ); $t_vcs = SourceVCS::repo( $t_repo ); $t_type = SourceType($t_repo->type); -$t_repo_commit_needs_issue = isset( $t_repo->info['repo_commit_needs_issue'] ) ? $t_repo->info['repo_commit_needs_issue'] : ''; -$t_repo_commit_issues_must_exist = isset( $t_repo->info['repo_commit_issues_must_exist'] ) ? $t_repo->info['repo_commit_issues_must_exist'] : ''; -$t_repo_commit_ownership_must_match = isset( $t_repo->info['repo_commit_ownership_must_match'] ) ? $t_repo->info['repo_commit_ownership_must_match'] : ''; -$t_repo_commit_status_restricted = isset( $t_repo->info['repo_commit_status_restricted'] ) ? $t_repo->info['repo_commit_status_restricted'] : ''; -$t_repo_commit_status_allowed = isset( $t_repo->info['repo_commit_status_allowed'] ) ? $t_repo->info['repo_commit_status_allowed'] : ''; -$t_repo_commit_project_restricted = isset( $t_repo->info['repo_commit_project_restricted'] ) ? $t_repo->info['repo_commit_project_restricted'] : ''; -$t_repo_commit_project_allowed = isset( $t_repo->info['repo_commit_project_allowed'] ) ? $t_repo->info['repo_commit_project_allowed'] : ''; +$t_repo_commit_needs_issue = isset( $t_repo->info['repo_commit_needs_issue'] ) ? $t_repo->info['repo_commit_needs_issue'] : false; +$t_repo_commit_issues_must_exist = isset( $t_repo->info['repo_commit_issues_must_exist'] ) ? $t_repo->info['repo_commit_issues_must_exist'] : false; +$t_repo_commit_ownership_must_match = isset( $t_repo->info['repo_commit_ownership_must_match'] ) ? $t_repo->info['repo_commit_ownership_must_match'] : false; +$t_repo_commit_committer_must_be_member = isset( $t_repo->info['repo_commit_committer_must_be_member'] ) ? $t_repo->info['repo_commit_committer_must_be_member'] : false; +$t_repo_commit_committer_must_be_level = isset( $t_repo->info['repo_commit_committer_must_be_level'] ) ? $t_repo->info['repo_commit_committer_must_be_level'] : MantisEnum::getValues( config_get( 'access_levels_enum_string' ) ) ; +$t_repo_commit_status_restricted = isset( $t_repo->info['repo_commit_status_restricted'] ) ? $t_repo->info['repo_commit_status_restricted'] : false; +$t_repo_commit_status_allowed = isset( $t_repo->info['repo_commit_status_allowed'] ) ? $t_repo->info['repo_commit_status_allowed'] : MantisEnum::getValues( config_get( 'status_enum_string' )); +$t_repo_commit_project_restricted = isset( $t_repo->info['repo_commit_project_restricted'] ) ? $t_repo->info['repo_commit_project_restricted'] : false; +$t_repo_commit_project_allowed = isset( $t_repo->info['repo_commit_project_allowed'] ) ? $t_repo->info['repo_commit_project_allowed'] : Array( 0 ); html_page_top1( plugin_lang_get( 'title' ) ); html_page_top2(); + +if( ON == config_get( 'use_javascript' ) ) +{ + $t_useJS = ' onclick="Source_Update_CheckOpts();" '; + html_javascript_link( 'addLoadEvent.js' ); +?> + +
@@ -71,24 +103,34 @@ /> +> + + id="repo_commit_committer_must_be_member" name="repo_commit_committer_must_be_member" type="checkbox" /> + + +> + + + + > -/> + name="repo_commit_status_restricted" id="repo_commit_status_restricted" type="checkbox" /> -> +> - + > -/> + id="repo_commit_project_restricted" name="repo_commit_project_restricted" type="checkbox" /> > - + diff --git a/SourceSVN/pre-commit.tmpl.mantis-checks-commit b/SourceSVN/pre-commit.tmpl.mantis-checks-commit index 227f1c32a..d16a5c159 100755 --- a/SourceSVN/pre-commit.tmpl.mantis-checks-commit +++ b/SourceSVN/pre-commit.tmpl.mantis-checks-commit @@ -2,22 +2,29 @@ # Copyright (c) 2014 John Bailey # Licensed under the MIT license +# # Requires GNU grep with PCRE +# Customise the following settings based on your Mantis installation URL="http://localhost/mantis/plugin.php?page=Source/pre_commit_check" PROJECT="RepoName" API_KEY="dd" + SVNLOOK=/usr/bin/svnlook CURL=/usr/bin/curl +# Deal with hook parameters REPOS="$1" TXN="$2" + LOG_FILE=`mktemp /tmp/svn_log.XXX` COMMENT_FILE=`mktemp /tmp/svn_comment.XXX` # Exit as soon as an error is encountered #set -e +# Check for tools + if [ ! -x "$SVNLOOK" ]; then echo "You need to update the script at $0 to point to svnlook" 1>&2 exit 1 @@ -53,6 +60,7 @@ elif [ $RESULT -eq 0 ]; then fi exit 1 else + # Success! # TODO exit 1 fi From c4b1bc889ae4d93280639b9b727b6cb892cf42fc Mon Sep 17 00:00:00 2001 From: bright-tools Date: Fri, 30 May 2014 21:48:57 +0100 Subject: [PATCH 16/36] Undo debugging change --- SourceSVN/pre-commit.tmpl.mantis-checks-commit | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/SourceSVN/pre-commit.tmpl.mantis-checks-commit b/SourceSVN/pre-commit.tmpl.mantis-checks-commit index d16a5c159..2dd9b6ba8 100755 --- a/SourceSVN/pre-commit.tmpl.mantis-checks-commit +++ b/SourceSVN/pre-commit.tmpl.mantis-checks-commit @@ -13,6 +13,10 @@ API_KEY="dd" SVNLOOK=/usr/bin/svnlook CURL=/usr/bin/curl +# Set this to 1 during testing so that even commits which pass the +# mantis checks will be halted +BOUNCE_ALL_COMMITS=0 + # Deal with hook parameters REPOS="$1" TXN="$2" @@ -61,8 +65,7 @@ elif [ $RESULT -eq 0 ]; then exit 1 else # Success! - # TODO - exit 1 + exit $BOUNCE_ALL_COMMITS fi exit 1 From 6a765ab1f1bf710c9189f3989d71b08c41aaff92 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Fri, 13 Jun 2014 19:28:30 +0100 Subject: [PATCH 17/36] Remove "repo checks commit" functionality --- Source/pages/issue_query.php | 47 ----------- SourceSVN/pre-commit.tmpl.repo-checks-commit | 89 -------------------- 2 files changed, 136 deletions(-) delete mode 100755 Source/pages/issue_query.php delete mode 100755 SourceSVN/pre-commit.tmpl.repo-checks-commit diff --git a/Source/pages/issue_query.php b/Source/pages/issue_query.php deleted file mode 100755 index dc39683e3..000000000 --- a/Source/pages/issue_query.php +++ /dev/null @@ -1,47 +0,0 @@ -project_id ) ); - printf("Issue-%s-User: '%s'\r\n",$t_bug_id_str,user_get_name( $t_bug->handler_id ) ); - printf("Issue-%s-Resolved: '%s'\r\n",$t_bug_id_str,$t_bug->status < $t_resolved_threshold ); - } - else - { - printf("Issue-%s-Exists: 0\r\n",$t_bug_id_str ); - } -} -printf("Issue-Count: %s\r\n",$t_bug_count ); - -?> diff --git a/SourceSVN/pre-commit.tmpl.repo-checks-commit b/SourceSVN/pre-commit.tmpl.repo-checks-commit deleted file mode 100755 index e81d7d950..000000000 --- a/SourceSVN/pre-commit.tmpl.repo-checks-commit +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/sh - -# Copyright (c) 2014 John Bailey -# Licensed under the MIT license -# Requires GNU grep with PCRE - -URL="http://localhost/mantis/plugin.php?page=Source/issue_query" -API_KEY="dd" -SVNLOOK=/usr/bin/svnlook -CURL=/usr/bin/curl - -MY_PROJECT="Tep" - -REPOS="$1" -TXN="$2" -LOG_FILE=`mktemp /tmp/svn_log.XXX` -COMMENT_FILE=`mktemp /tmp/svn_comment.XXX` - -# Exit as soon as an error is encountered -#set -e - - -check_commit() -{ - COMMITTER=$1 - PROJECT=$2 - TICKET_OWNER=$3 - # TODO: Check the ticket's status - - echo "Checking $COMMITTER:$PROJECT:$TICKET_OWNER" 1>&2 - # You will probably need to modify the checks below depending on your requirements - - # TODO: Check how this works for sub-projects - if [ "x$MY_PROJECT" != "x$PROJECT" ]; then - echo "Ticket belongs to project '$PROJECT'. Was expecting this to be '$MY_PROJECT'" 1>&2 - exit 1 - fi; - if [ "x$COMMITTER" != "x$TICKET_OWNER" ]; then - echo "Ticket is assigned to '$TICKET_OWNER'. You're attempting to commit as '$COMMITTER'" 1>&2 - exit 1 - fi; -} - -if [ ! -x "$SVNLOOK" ]; then - echo "You need to update the script at $0 to point to svnlook" 1>&2 - exit 1 -fi - -if [ ! -x "$CURL" ]; then - echo "You need to update the script at $0 to point to curl" 1>&2 - exit 1 -fi - -echo 'commit_comment="' >> ${COMMENT_FILE} -"${SVNLOOK}" log -t "${TXN}" "${REPOS}" >> ${COMMENT_FILE} -echo '"' >> ${COMMENT_FILE} -"${CURL}" -d @${COMMENT_FILE} -d "api_key=${API_KEY}" ${URL} >> ${LOG_FILE} -COMMITTING_USER=$("${SVNLOOK}" author -t "${TXN}" "${REPOS}") - -COUNT=$(grep -oP 'Issue-Count: \K([0-9]+)' ${LOG_FILE}) - -if [ "x$COUNT" = "x" ]; then - echo "Didn't get a valid response from Mantis SourceIntegration" 1>&2 - exit 1 -elif [ $COUNT -eq 0 ]; then - echo "You need to reference at least one issue in your commit comment" 1>&2 - exit 1 -else - i=0 - for i in $(seq 1 $COUNT) - do - NUM=$(printf "%08d" $i) - ID=$(grep -oP "Issue-$NUM-ID: \K([a-zA-Z0-9_]+)" ${LOG_FILE}) - EXISTS=$(grep -oP "Issue-$NUM-Exists: \K([a-zA-Z0-9_]+)" ${LOG_FILE}) - - if [ $EXISTS -eq 1 ]; then - PROJECT=$(grep -oP "Issue-$NUM-Project: '\K(.+)(?=')" ${LOG_FILE}) - USER=$(grep -oP "Issue-$NUM-User: '\K([a-zA-Z0-9_.+-]+)(?=')" ${LOG_FILE}) - # TODO: Sanity check all the parameters before calling check_commit - check_commit "$COMMITTING_USER" "$PROJECT" "$USER" - else - echo "Issue ID $ID doesn't seem to exist in Mantis (exists:'$EXISTS')" 1>&2 - exit 1 - fi - done -fi - -exit 1 - From 83237c0463a016b74878ca864cd63502efd522c1 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Sat, 14 Jun 2014 11:24:31 +0100 Subject: [PATCH 18/36] Close out TODOs and add options to suppress information in commit bounces --- Source/pages/pre_commit_check.php | 112 +++++++++++++++++++----------- 1 file changed, 70 insertions(+), 42 deletions(-) diff --git a/Source/pages/pre_commit_check.php b/Source/pages/pre_commit_check.php index 5410e6cc2..1ad12af94 100755 --- a/Source/pages/pre_commit_check.php +++ b/Source/pages/pre_commit_check.php @@ -9,11 +9,14 @@ include_once 'si-common.php'; -if (! si_is_key_ok() ) { -# TODO: Does this error make it back via the hook? +if ( !si_is_key_ok() ) { die( plugin_lang_get( 'invalid_key' ) ); } +# If you're worried about information "leaking" out via error messages, set this to false to prevent error messages +# containing any information from the ticket(s) +$t_informational_errors = false; + # Get a list of the bug IDs which were referenced in the commit comment $t_bug_list = Source_Parse_Buglinks( gpc_get_string( 'commit_comment', '' )); $t_resolved_threshold = config_get('bug_resolved_status_threshold'); @@ -23,7 +26,6 @@ $t_repo = SourceRepo::load_by_name( $f_repo_name ); # Repo not found if ( is_null( $t_repo ) ) { -# TODO: Does this error make it back via the hook? die( plugin_lang_get( 'invalid_repo' ) ); } $t_repo_commit_needs_issue = isset( $t_repo->info['repo_commit_needs_issue'] ) ? $t_repo->info['repo_commit_needs_issue'] : false; @@ -61,14 +63,20 @@ $t_user_name = user_get_name( $t_bug->handler_id ); $t_user_email = user_get_email( $t_bug->handler_id ); -# TODO: Check vs e-mail address - # Check that the username of the committer matches the user name - # of the owner of the ticket - if(!( strlen( $f_committer_name ) && ( $t_user_name == $f_committer_name ))) + # or e-mail address of the owner of the ticket + if(!( strlen( $f_committer_name ) && + (( $t_user_name == $f_committer_name ) || + ( $t_user_email == $f_committer_name )))) { - printf("Check-Message: '%s : %d (%s vs %s)'\r\n", - plugin_lang_get( 'error_commit_issue_ownership' ), $t_bug_id, $t_user_name, $f_committer_name ); + printf("Check-Message: '%s : %d", plugin_lang_get( 'error_commit_issue_ownership' ), $t_bug_id ); + + if( $t_informational_errors ) + { + printf(" (%s vs %s/%s)", + $t_user_name, $f_committer_name, $t_user_email ); + } + printf("'\r\n"); $t_all_ok = false; } } @@ -78,21 +86,28 @@ # is allowed if( !in_array( $t_bug->status, $t_repo_commit_status_allowed )) { - printf("Check-Message: '%s : %d (%s vs ", - plugin_lang_get( 'error_commit_issue_wrong_status' ), $t_bug_id, get_enum_element( 'status', $t_bug->status )); - $t_first = true; + printf("Check-Message: '%s : %d", + plugin_lang_get( 'error_commit_issue_wrong_status' ), $t_bug_id ); - # Output the list of statuses for which commit is allowed - foreach( $t_repo_commit_status_allowed as $t_allowed_status ) + if( $t_informational_errors ) { - if( !$t_first ) + printf(" (%s vs ", get_enum_element( 'status', $t_bug->status )); + + $t_first = true; + + # Output the list of statuses for which commit is allowed + foreach( $t_repo_commit_status_allowed as $t_allowed_status ) { - printf(", "); + if( !$t_first ) + { + printf(", "); + } + printf( get_enum_element( 'status', $t_allowed_status )); + $t_first = false; } - printf( get_enum_element( 'status', $t_allowed_status )); - $t_first = false; + printf(")"); } - printf(")'\r\n"); + printf("'\r\n"); $t_all_ok = false; } } @@ -101,21 +116,27 @@ if( !in_array( 0, $t_repo_commit_project_allowed ) && !in_array( $t_bug->project_id, $t_repo_commit_project_allowed )) { - printf("Check-Message: '%s : %d (%s vs ", - plugin_lang_get( 'error_commit_issue_wrong_project' ), $t_bug_id, project_get_field( $t_bug->project_id, 'name' )); - $t_first = true; - - # Output the list of projects for which commit is allowed - foreach( $t_repo_commit_project_allowed as $t_allowed_project ) + printf("Check-Message: '%s : %d", + plugin_lang_get( 'error_commit_issue_wrong_project' ), $t_bug_id ); + if( $t_informational_errors ) { - if( !$t_first ) + printf(" (%s vs ", project_get_field( $t_bug->project_id, 'name' )); + + $t_first = true; + + # Output the list of projects for which commit is allowed + foreach( $t_repo_commit_project_allowed as $t_allowed_project ) { - printf(", "); + if( !$t_first ) + { + printf(", "); + } + printf( project_get_field( $t_allowed_project, 'name' ) ); + $t_first = false; } - printf( project_get_field( $t_allowed_project, 'name' ) ); - $t_first = false; + printf(")"); } - printf(")'\r\n"); + printf("'\r\n"); $t_all_ok = false; } } @@ -142,22 +163,29 @@ $t_user_access_level = project_get_local_user_access_level( $t_bug->project_id, $t_user_id ); if( !in_array( $t_user_access_level, $t_repo_commit_committer_must_be_level )) { - printf("Check-Message: '%s : %d (%s vs", - plugin_lang_get( 'error_commit_committer_wrong_level' ), $t_bug_id, $f_committer_name ); - $t_first = true; - $t_levels = MantisEnum::getValues( config_get( 'access_levels_enum_string' ) ); - # Output the list of projects for which commit is allowed - foreach( $t_repo_commit_committer_must_be_level as $t_allowed_level ) + printf("Check-Message: '%s : %d", + plugin_lang_get( 'error_commit_committer_wrong_level' ), $t_bug_id ); + + if( $t_informational_errors ) { - if( !$t_first ) + printf(" (%s vs", $f_committer_name ); + + $t_first = true; + $t_levels = MantisEnum::getValues( config_get( 'access_levels_enum_string' ) ); + # Output the list of projects for which commit is allowed + foreach( $t_repo_commit_committer_must_be_level as $t_allowed_level ) { - printf(", "); + if( !$t_first ) + { + printf(", "); + } + printf( $t_levels[ $t_allowed_level ] ); + $t_first = false; } - printf( $t_levels[ $t_allowed_level ] ); - $t_first = false; - } - printf(")'\r\n"); + printf(")"); + } + printf("'\r\n"); $t_all_ok = false; } } From 894fd73a9786e56fb9cde2afc118695b2cd6c6ee Mon Sep 17 00:00:00 2001 From: bright-tools Date: Sat, 14 Jun 2014 11:25:39 +0100 Subject: [PATCH 19/36] Make errors more informational and make sure that they're output by the hook script --- Source/lang/strings_english.txt | 4 ++-- SourceSVN/pre-commit.tmpl.mantis-checks-commit | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Source/lang/strings_english.txt b/Source/lang/strings_english.txt index bc7c76767..1369b5bb9 100755 --- a/Source/lang/strings_english.txt +++ b/Source/lang/strings_english.txt @@ -147,9 +147,9 @@ $s_plugin_Source_import_repo_error = 'Import process produced an error.'; $s_plugin_Source_invalid_checkin_url = 'Invalid remote check-in address'; $s_plugin_Source_invalid_import_url = 'Invalid remote import address'; -$s_plugin_Source_invalid_repo = 'Invalid repository name'; +$s_plugin_Source_invalid_repo = 'Invalid repository name. Please check that you have set the PROJECT variable correctly in the hook script'; $s_plugin_Source_invalid_changeset = 'Changeset information could not be loaded'; -$s_plugin_Source_invalid_key = 'Invalid API Key'; +$s_plugin_Source_invalid_key = 'Invalid API Key. Please check that you have set the API_KEY variable correctly in the hook script'; $s_plugin_Source_import_latest_failed = 'Repository latest data importing failed.'; $s_plugin_Source_import_full_failed = 'Full repository data importing failed.'; diff --git a/SourceSVN/pre-commit.tmpl.mantis-checks-commit b/SourceSVN/pre-commit.tmpl.mantis-checks-commit index 2dd9b6ba8..562bdd053 100755 --- a/SourceSVN/pre-commit.tmpl.mantis-checks-commit +++ b/SourceSVN/pre-commit.tmpl.mantis-checks-commit @@ -53,7 +53,12 @@ echo '"' >> ${COMMENT_FILE} RESULT=$(grep -oP 'Check-OK: \K([0-9]+)' ${LOG_FILE}) if [ "x$RESULT" = "x" ]; then - echo "Didn't get a valid response from Mantis SourceIntegration" 1>&2 + echo "Didn't receive a valid response from Mantis SourceIntegration" 1>&2 + echo "Received:" 1>&2 + echo "-----------8<--------------------" 1>&2 + cat ${LOG_FILE} 1>&2 + echo 1>&2 + echo "-----------8<--------------------" 1>&2 exit 1; elif [ $RESULT -eq 0 ]; then CHECK_MESSAGE=$(grep -oP "Check-Message: '\K(.+)(?=')" ${LOG_FILE}) From 373e84a84d58508935a904b9e58cae3c50439f2d Mon Sep 17 00:00:00 2001 From: bright-tools Date: Mon, 16 Jun 2014 20:41:08 +0100 Subject: [PATCH 20/36] Add note regarding making commit errors more or less informational --- CommitMessageCheck.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CommitMessageCheck.md b/CommitMessageCheck.md index 89b41d974..68b05ca0f 100755 --- a/CommitMessageCheck.md +++ b/CommitMessageCheck.md @@ -107,3 +107,7 @@ it recognises comments containing text such as `issue #12` or `issue #12,#61`, e That's unfortunate and will require some manual intervention. Possibly the easiest way to work around this is to create a look-up function to return the Mantis user-name based on the version control user name then modify the hook function to call this. +*I'm worried that information will 'leak' from the Mantis database - maybe someone could use this functionality to extract private information from Mantis?* + +The Mantis installation and the VCS repository need to be set up to share a private key (as per SourceIntegration). This means that someone without access to the API key will not be able to access the functionality used to check commit messages. +In the case that someone has access to the API (either by having access to the private key or by having commit access to an associated repository) then negative responses are intended to be informational in order to assist the user in understanding why the commit was refused. This may mean that information relating to access levels or user names may be shown in the refusal message. If this is a concern, changing the value of `$t_informational_errors` from `true` to `false` in `Source/pages/pre_commit_check.php` will restrict the information to a minimum. From d40c54c4be35f08aa7d06e1ed41149a6a22f5a28 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Mon, 16 Jun 2014 22:41:35 +0100 Subject: [PATCH 21/36] Ensure that line-breaks are preserved in output --- SourceSVN/pre-commit.tmpl.mantis-checks-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SourceSVN/pre-commit.tmpl.mantis-checks-commit b/SourceSVN/pre-commit.tmpl.mantis-checks-commit index 562bdd053..4d76069d8 100755 --- a/SourceSVN/pre-commit.tmpl.mantis-checks-commit +++ b/SourceSVN/pre-commit.tmpl.mantis-checks-commit @@ -65,7 +65,7 @@ elif [ $RESULT -eq 0 ]; then if [ "x$CHECK_MESSAGE" = "x" ]; then echo "Mantis SourceIntegration bounced the commit but didn't report why" 1>&2 else - echo $CHECK_MESSAGE 1>&2 + echo "$CHECK_MESSAGE" 1>&2 fi exit 1 else From 912b61c435337ecf0246e74e4b0f4fe27b3c351d Mon Sep 17 00:00:00 2001 From: bright-tools Date: Mon, 16 Jun 2014 23:28:09 +0100 Subject: [PATCH 22/36] Fix typo in option name --- Source/pages/repo_update.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/pages/repo_update.php b/Source/pages/repo_update.php index 112d2f220..d999d3659 100755 --- a/Source/pages/repo_update.php +++ b/Source/pages/repo_update.php @@ -16,7 +16,7 @@ $f_repo_commit_status_allowed = gpc_get_int_array( 'repo_commit_status_allowed', MantisEnum::getValues( config_get( 'status_enum_string' ) )); $f_repo_commit_project_restricted = gpc_get_bool( 'repo_commit_project_restricted', false ); $f_repo_commit_project_allowed = gpc_get_int_array( 'repo_commit_project_allowed', Array( 0 ) ); -$f_repo_commit_committer_must_be_member = gpc_get_bool( 'repo_commit_committer_must_me_member', false ); +$f_repo_commit_committer_must_be_member = gpc_get_bool( 'repo_commit_committer_must_be_member', false ); $f_repo_commit_committer_must_be_level = gpc_get_int_array( 'repo_commit_committer_must_be_level', MantisEnum::getValues( config_get( 'access_levels_enum_string' ) )); $t_repo = SourceRepo::load( $f_repo_id ); From 77f217b306d540ac57d60bf3760b8ea7764681a2 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Mon, 16 Jun 2014 23:47:43 +0100 Subject: [PATCH 23/36] Use array_map() and implode() to avoid long-winded loops --- Source/pages/pre_commit_check.php | 82 ++++++++++++++----------------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/Source/pages/pre_commit_check.php b/Source/pages/pre_commit_check.php index 1ad12af94..4226a80e8 100755 --- a/Source/pages/pre_commit_check.php +++ b/Source/pages/pre_commit_check.php @@ -15,7 +15,7 @@ # If you're worried about information "leaking" out via error messages, set this to false to prevent error messages # containing any information from the ticket(s) -$t_informational_errors = false; +$t_informational_errors = true; # Get a list of the bug IDs which were referenced in the commit comment $t_bug_list = Source_Parse_Buglinks( gpc_get_string( 'commit_comment', '' )); @@ -51,6 +51,7 @@ } else { + # Loop all the bug IDs referenced in the commit comment foreach( $t_bug_list as $t_bug_id ) { # Check existence first to prevent API throwing an error @@ -58,6 +59,7 @@ { $t_bug = bug_get( $t_bug_id ); + # Ownership of ticket must match committer? if( $t_repo_commit_ownership_must_match ) { $t_user_name = user_get_name( $t_bug->handler_id ); @@ -73,13 +75,18 @@ if( $t_informational_errors ) { + # Informative errors turned on so display the user to whom + # the ticket is assigned printf(" (%s vs %s/%s)", $t_user_name, $f_committer_name, $t_user_email ); } + printf("'\r\n"); $t_all_ok = false; } } + + # Only allowed to commit against tickets with a specific status? if( $t_repo_commit_status_restricted ) { # Check that the bug's status is at a level for which a commit @@ -91,26 +98,22 @@ if( $t_informational_errors ) { - printf(" (%s vs ", get_enum_element( 'status', $t_bug->status )); - - $t_first = true; - - # Output the list of statuses for which commit is allowed - foreach( $t_repo_commit_status_allowed as $t_allowed_status ) - { - if( !$t_first ) - { - printf(", "); - } - printf( get_enum_element( 'status', $t_allowed_status )); - $t_first = false; - } - printf(")"); + # Informative errors turned on so display a list of statuses for which + # a commit would be accepted + + # Get an array of the names of the statuses for which commit is allowed + $t_statuses = array_map( function( $p_status ) { return get_enum_element( 'status', $p_status ); }, $t_repo_commit_status_allowed ); + + printf(" (%s vs %s)", + get_enum_element( 'status', $t_bug->status ), + implode( $t_statuses, ", " )); } printf("'\r\n"); $t_all_ok = false; } } + + # Only allowed to commit against Mantis tickets within specific project(s) if( $t_repo_commit_project_restricted ) { if( !in_array( 0, $t_repo_commit_project_allowed ) && @@ -118,24 +121,20 @@ { printf("Check-Message: '%s : %d", plugin_lang_get( 'error_commit_issue_wrong_project' ), $t_bug_id ); + if( $t_informational_errors ) { - printf(" (%s vs ", project_get_field( $t_bug->project_id, 'name' )); - - $t_first = true; - - # Output the list of projects for which commit is allowed - foreach( $t_repo_commit_project_allowed as $t_allowed_project ) - { - if( !$t_first ) - { - printf(", "); - } - printf( project_get_field( $t_allowed_project, 'name' ) ); - $t_first = false; - } - printf(")"); + # Informative errors turned on so display a list of Mantis projects to + # which referenced tickets must belong + + # Get an array of the names of all the projects + $t_projects = array_map( function( $p_proj ) { return project_get_field( $p_proj, 'name' ); }, $t_repo_commit_project_allowed ); + + printf(" (%s vs %s)", + project_get_field( $t_bug->project_id, 'name' ), + implode( $t_projects, ", " )); } + printf("'\r\n"); $t_all_ok = false; } @@ -168,22 +167,13 @@ if( $t_informational_errors ) { - printf(" (%s vs", $f_committer_name ); - $t_first = true; - $t_levels = MantisEnum::getValues( config_get( 'access_levels_enum_string' ) ); - # Output the list of projects for which commit is allowed - foreach( $t_repo_commit_committer_must_be_level as $t_allowed_level ) - { - if( !$t_first ) - { - printf(", "); - } - printf( $t_levels[ $t_allowed_level ] ); - $t_first = false; - } - - printf(")"); + $t_levels = MantisEnum::getAssocArrayIndexedByValues( config_get( 'access_levels_enum_string' ) ); + $t_allowed_levels = array_intersect_key( $t_levels, array_flip( $t_repo_commit_committer_must_be_level )); + + printf(" (%s vs %s)", + $t_levels[ $t_user_access_level ], + implode( array_values( $t_allowed_levels ), ", ")); } printf("'\r\n"); $t_all_ok = false; From 8ed9163acd0dfa81e566db57faba939fefb14fb3 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Tue, 17 Jun 2014 18:46:47 +0100 Subject: [PATCH 24/36] Update to try and match committer against project members by e-mail if username fails --- Source/pages/pre_commit_check.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Source/pages/pre_commit_check.php b/Source/pages/pre_commit_check.php index 4226a80e8..51591245e 100755 --- a/Source/pages/pre_commit_check.php +++ b/Source/pages/pre_commit_check.php @@ -139,10 +139,17 @@ $t_all_ok = false; } } + if( $t_repo_commit_committer_must_be_member ) { $t_user_id = user_get_id_by_name( $f_committer_name ); + # Didn't find the username? Try the e-mail address + if( $t_user_id == false ) + { + $t_user_id = user_get_id_by_email( $f_committer_name ); + } + /* Check that the user exists in Mantis */ if( $t_user_id == false ) { @@ -167,7 +174,9 @@ if( $t_informational_errors ) { - $t_first = true; + # Informative errors turned on so display a list of access levels + # for which commit is allowed + $t_levels = MantisEnum::getAssocArrayIndexedByValues( config_get( 'access_levels_enum_string' ) ); $t_allowed_levels = array_intersect_key( $t_levels, array_flip( $t_repo_commit_committer_must_be_level )); From 0175ee8977734f4e110724e447790332fcbb5818 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Tue, 17 Jun 2014 22:23:44 +0100 Subject: [PATCH 25/36] Tidy up error strings --- Source/pages/pre_commit_check.php | 41 +++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/Source/pages/pre_commit_check.php b/Source/pages/pre_commit_check.php index 51591245e..768c763bd 100755 --- a/Source/pages/pre_commit_check.php +++ b/Source/pages/pre_commit_check.php @@ -71,7 +71,10 @@ (( $t_user_name == $f_committer_name ) || ( $t_user_email == $f_committer_name )))) { - printf("Check-Message: '%s : %d", plugin_lang_get( 'error_commit_issue_ownership' ), $t_bug_id ); + printf("Check-Message: '%s : %s %d", + plugin_lang_get( 'error_commit_issue_ownership' ), + plugin_lang_get( 'issue' ), + $t_bug_id ); if( $t_informational_errors ) { @@ -93,8 +96,10 @@ # is allowed if( !in_array( $t_bug->status, $t_repo_commit_status_allowed )) { - printf("Check-Message: '%s : %d", - plugin_lang_get( 'error_commit_issue_wrong_status' ), $t_bug_id ); + printf("Check-Message: '%s : %s %d", + plugin_lang_get( 'error_commit_issue_wrong_status' ), + plugin_lang_get( 'issue' ), + $t_bug_id ); if( $t_informational_errors ) { @@ -119,8 +124,10 @@ if( !in_array( 0, $t_repo_commit_project_allowed ) && !in_array( $t_bug->project_id, $t_repo_commit_project_allowed )) { - printf("Check-Message: '%s : %d", - plugin_lang_get( 'error_commit_issue_wrong_project' ), $t_bug_id ); + printf("Check-Message: '%s : %s %d", + plugin_lang_get( 'error_commit_issue_wrong_project' ), + plugin_lang_get( 'issue' ), + $t_bug_id ); if( $t_informational_errors ) { @@ -153,15 +160,19 @@ /* Check that the user exists in Mantis */ if( $t_user_id == false ) { - printf("Check-Message: '%s : %d (%s)'\r\n", - plugin_lang_get( 'error_commit_committer_not_found' ), $t_bug_id, $f_committer_name ); + printf("Check-Message: '%s : %s %d (%s)'\r\n", + plugin_lang_get( 'error_commit_committer_not_found' ), + plugin_lang_get( 'issue' ), + $t_bug_id, $f_committer_name ); $t_all_ok = false; } /* Check that the user is assigned to the project */ elseif( ! project_includes_user( $t_bug->project_id, $t_user_id )) { - printf("Check-Message: '%s : %d (%s)'\r\n", - plugin_lang_get( 'error_commit_committer_not_member' ), $t_bug_id, $f_committer_name ); + printf("Check-Message: '%s : %s %d (%s)'\r\n", + plugin_lang_get( 'error_commit_committer_not_member' ), + plugin_lang_get( 'issue' ), + $t_bug_id, $f_committer_name ); $t_all_ok = false; } else @@ -169,8 +180,10 @@ $t_user_access_level = project_get_local_user_access_level( $t_bug->project_id, $t_user_id ); if( !in_array( $t_user_access_level, $t_repo_commit_committer_must_be_level )) { - printf("Check-Message: '%s : %d", - plugin_lang_get( 'error_commit_committer_wrong_level' ), $t_bug_id ); + printf("Check-Message: '%s : %s %d", + plugin_lang_get( 'error_commit_committer_wrong_level' ), + plugin_lang_get( 'issue' ), + $t_bug_id ); if( $t_informational_errors ) { @@ -199,8 +212,10 @@ $t_repo_commit_project_restricted || $t_repo_commit_committer_must_be_member ) { - printf("Check-Message: '%s : %d'\r\n", - plugin_lang_get( 'error_commit_nonexistent_issue' ), $t_bug_id ); + printf("Check-Message: '%s : %s %d'\r\n", + plugin_lang_get( 'error_commit_nonexistent_issue' ), + plugin_lang_get( 'issue' ), + $t_bug_id ); $t_all_ok = false; } } From ddef1cea4fa1eed4cbfa1a9f6731f2fd853f7441 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Tue, 17 Jun 2014 22:24:00 +0100 Subject: [PATCH 26/36] Add example of a bounced commit --- CommitMessageCheck.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CommitMessageCheck.md b/CommitMessageCheck.md index 68b05ca0f..698947afa 100755 --- a/CommitMessageCheck.md +++ b/CommitMessageCheck.md @@ -101,6 +101,18 @@ of comments. The format for referencing comments is configured as a regular expression as part of the Source Integration plugin configuration. By default it recognises comments containing text such as `issue #12` or `issue #12,#61`, etc. +### Example + + bright-tools@ubuntu:~/repo$ svn commit -m "Issue #1,#8: Fix up the typos" + Sending example.txt + Transmitting file data .svn: E165001: Commit failed (details follow): + svn: E165001: Commit blocked by pre-commit hook (exit code 1) with output: + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed + 100 778 100 699 100 79 44573 5037 --:--:-- --:--:-- --:--:-- 46600 + Committing user does not have appropriate access level in project : Issue 1 (updater vs manager, administrator) + Commit comment references non-existent issue : Issue 8 + ## Potential Problems *The user-names used for version control don't match those used in Mantis* From c99bd59776946cd7995ae8ac8b1dd53dd65beb7b Mon Sep 17 00:00:00 2001 From: bright-tools Date: Thu, 21 Aug 2014 22:02:11 +0100 Subject: [PATCH 27/36] Doc tweak --- CommitMessageCheck.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CommitMessageCheck.md b/CommitMessageCheck.md index 698947afa..8223a660f 100755 --- a/CommitMessageCheck.md +++ b/CommitMessageCheck.md @@ -112,6 +112,7 @@ it recognises comments containing text such as `issue #12` or `issue #12,#61`, e 100 778 100 699 100 79 44573 5037 --:--:-- --:--:-- --:--:-- 46600 Committing user does not have appropriate access level in project : Issue 1 (updater vs manager, administrator) Commit comment references non-existent issue : Issue 8 + bright-tools@ubuntu:~/repo$ ## Potential Problems From 6c10f14cb6b7f89216fb072958445d558600c367 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Thu, 21 Aug 2014 22:31:00 +0100 Subject: [PATCH 28/36] Update to better fit in with Mantis coding guidelines --- Source/pages/{si-common.php => si_common.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Source/pages/{si-common.php => si_common.php} (100%) diff --git a/Source/pages/si-common.php b/Source/pages/si_common.php similarity index 100% rename from Source/pages/si-common.php rename to Source/pages/si_common.php From ba952808e9d5edc85015f1b770355f43ae1f9452 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Thu, 21 Aug 2014 22:35:37 +0100 Subject: [PATCH 29/36] Update to better fit in with Mantis coding guidelines --- Source/pages/pre_commit_check.php | 365 +++++++++++++++--------------- Source/pages/repo_update_page.php | 5 +- Source/pages/si_common.php | 6 +- 3 files changed, 187 insertions(+), 189 deletions(-) diff --git a/Source/pages/pre_commit_check.php b/Source/pages/pre_commit_check.php index 768c763bd..3ec148eb7 100755 --- a/Source/pages/pre_commit_check.php +++ b/Source/pages/pre_commit_check.php @@ -7,10 +7,10 @@ # mantis ticket references in the commit comment. The level of checking # is configured on a per-repository basis -include_once 'si-common.php'; +include_once 'si_common.php'; if ( !si_is_key_ok() ) { - die( plugin_lang_get( 'invalid_key' ) ); + die( plugin_lang_get( 'invalid_key' ) ); } # If you're worried about information "leaking" out via error messages, set this to false to prevent error messages @@ -19,14 +19,14 @@ # Get a list of the bug IDs which were referenced in the commit comment $t_bug_list = Source_Parse_Buglinks( gpc_get_string( 'commit_comment', '' )); -$t_resolved_threshold = config_get('bug_resolved_status_threshold'); +$t_resolved_threshold = config_get( 'bug_resolved_status_threshold' ); $f_committer_name = gpc_get_string( 'committer', '' ); $f_repo_name = gpc_get_string( 'repo_name', '' ); $t_repo = SourceRepo::load_by_name( $f_repo_name ); # Repo not found if ( is_null( $t_repo ) ) { - die( plugin_lang_get( 'invalid_repo' ) ); + die( plugin_lang_get( 'invalid_repo' ) ); } $t_repo_commit_needs_issue = isset( $t_repo->info['repo_commit_needs_issue'] ) ? $t_repo->info['repo_commit_needs_issue'] : false; $t_repo_commit_issues_must_exist = isset( $t_repo->info['repo_commit_issues_must_exist'] ) ? $t_repo->info['repo_commit_issues_must_exist'] : false; @@ -41,186 +41,185 @@ $t_all_ok = true; # Check number of bugs referenced in the commit comment -if(( sizeof( $t_bug_list ) == 0 ) && $t_repo_commit_needs_issue ) -{ - # It was expected that the commit comment would reference one of more bug - # IDs but this was not the case +if(( sizeof( $t_bug_list ) == 0 ) && $t_repo_commit_needs_issue ) { - printf("Check-Message: '%s'\r\n",plugin_lang_get( 'error_commit_needs_issue' ) ); - $t_all_ok = false; -} -else -{ - # Loop all the bug IDs referenced in the commit comment - foreach( $t_bug_list as $t_bug_id ) - { - # Check existence first to prevent API throwing an error - if( bug_exists( $t_bug_id ) ) - { - $t_bug = bug_get( $t_bug_id ); - - # Ownership of ticket must match committer? - if( $t_repo_commit_ownership_must_match ) - { - $t_user_name = user_get_name( $t_bug->handler_id ); - $t_user_email = user_get_email( $t_bug->handler_id ); - - # Check that the username of the committer matches the user name - # or e-mail address of the owner of the ticket - if(!( strlen( $f_committer_name ) && - (( $t_user_name == $f_committer_name ) || - ( $t_user_email == $f_committer_name )))) - { - printf("Check-Message: '%s : %s %d", - plugin_lang_get( 'error_commit_issue_ownership' ), - plugin_lang_get( 'issue' ), - $t_bug_id ); - - if( $t_informational_errors ) - { - # Informative errors turned on so display the user to whom - # the ticket is assigned - printf(" (%s vs %s/%s)", - $t_user_name, $f_committer_name, $t_user_email ); - } - - printf("'\r\n"); - $t_all_ok = false; - } - } - - # Only allowed to commit against tickets with a specific status? - if( $t_repo_commit_status_restricted ) - { - # Check that the bug's status is at a level for which a commit - # is allowed - if( !in_array( $t_bug->status, $t_repo_commit_status_allowed )) - { - printf("Check-Message: '%s : %s %d", - plugin_lang_get( 'error_commit_issue_wrong_status' ), - plugin_lang_get( 'issue' ), - $t_bug_id ); - - if( $t_informational_errors ) - { - # Informative errors turned on so display a list of statuses for which - # a commit would be accepted - - # Get an array of the names of the statuses for which commit is allowed - $t_statuses = array_map( function( $p_status ) { return get_enum_element( 'status', $p_status ); }, $t_repo_commit_status_allowed ); - - printf(" (%s vs %s)", - get_enum_element( 'status', $t_bug->status ), - implode( $t_statuses, ", " )); - } - printf("'\r\n"); - $t_all_ok = false; - } - } - - # Only allowed to commit against Mantis tickets within specific project(s) - if( $t_repo_commit_project_restricted ) - { - if( !in_array( 0, $t_repo_commit_project_allowed ) && - !in_array( $t_bug->project_id, $t_repo_commit_project_allowed )) - { - printf("Check-Message: '%s : %s %d", - plugin_lang_get( 'error_commit_issue_wrong_project' ), - plugin_lang_get( 'issue' ), - $t_bug_id ); - - if( $t_informational_errors ) - { - # Informative errors turned on so display a list of Mantis projects to - # which referenced tickets must belong - - # Get an array of the names of all the projects - $t_projects = array_map( function( $p_proj ) { return project_get_field( $p_proj, 'name' ); }, $t_repo_commit_project_allowed ); - - printf(" (%s vs %s)", - project_get_field( $t_bug->project_id, 'name' ), - implode( $t_projects, ", " )); - } - - printf("'\r\n"); - $t_all_ok = false; - } - } - - if( $t_repo_commit_committer_must_be_member ) - { - $t_user_id = user_get_id_by_name( $f_committer_name ); - - # Didn't find the username? Try the e-mail address - if( $t_user_id == false ) - { - $t_user_id = user_get_id_by_email( $f_committer_name ); - } - - /* Check that the user exists in Mantis */ - if( $t_user_id == false ) - { - printf("Check-Message: '%s : %s %d (%s)'\r\n", - plugin_lang_get( 'error_commit_committer_not_found' ), - plugin_lang_get( 'issue' ), - $t_bug_id, $f_committer_name ); - $t_all_ok = false; - } - /* Check that the user is assigned to the project */ - elseif( ! project_includes_user( $t_bug->project_id, $t_user_id )) - { - printf("Check-Message: '%s : %s %d (%s)'\r\n", - plugin_lang_get( 'error_commit_committer_not_member' ), - plugin_lang_get( 'issue' ), - $t_bug_id, $f_committer_name ); - $t_all_ok = false; - } - else - { - $t_user_access_level = project_get_local_user_access_level( $t_bug->project_id, $t_user_id ); - if( !in_array( $t_user_access_level, $t_repo_commit_committer_must_be_level )) - { - printf("Check-Message: '%s : %s %d", - plugin_lang_get( 'error_commit_committer_wrong_level' ), - plugin_lang_get( 'issue' ), - $t_bug_id ); - - if( $t_informational_errors ) - { - # Informative errors turned on so display a list of access levels - # for which commit is allowed - - $t_levels = MantisEnum::getAssocArrayIndexedByValues( config_get( 'access_levels_enum_string' ) ); - $t_allowed_levels = array_intersect_key( $t_levels, array_flip( $t_repo_commit_committer_must_be_level )); - - printf(" (%s vs %s)", - $t_levels[ $t_user_access_level ], - implode( array_values( $t_allowed_levels ), ", ")); - } - printf("'\r\n"); - $t_all_ok = false; - } - } - } - } - else - { - /* If the issue doesn't exist, then can't perform the checks */ - if( $t_repo_commit_issues_must_exist || - $t_repo_commit_ownership_must_match || - $t_repo_commit_status_restricted || - $t_repo_commit_project_restricted || - $t_repo_commit_committer_must_be_member ) - { - printf("Check-Message: '%s : %s %d'\r\n", - plugin_lang_get( 'error_commit_nonexistent_issue' ), - plugin_lang_get( 'issue' ), - $t_bug_id ); - $t_all_ok = false; - } - } - } + # It was expected that the commit comment would reference one of more bug + # IDs but this was not the case + + printf( "Check-Message: '%s'\r\n",plugin_lang_get( 'error_commit_needs_issue' ) ); + $t_all_ok = false; + +} else { + + # Loop all the bug IDs referenced in the commit comment + foreach( $t_bug_list as $t_bug_id ) { + + # Check existence first to prevent API throwing an error + if( bug_exists( $t_bug_id ) ) { + + $t_bug = bug_get( $t_bug_id ); + + # Ownership of ticket must match committer? + if( $t_repo_commit_ownership_must_match ) { + + $t_user_name = user_get_name( $t_bug->handler_id ); + $t_user_email = user_get_email( $t_bug->handler_id ); + + # Check that the username of the committer matches the user name + # or e-mail address of the owner of the ticket + if( !( strlen( $f_committer_name ) && + (( $t_user_name == $f_committer_name ) || + ( $t_user_email == $f_committer_name )))) { + + printf( "Check-Message: '%s : %s %d", + plugin_lang_get( 'error_commit_issue_ownership' ), + plugin_lang_get( 'issue' ), + $t_bug_id ); + + if( $t_informational_errors ) { + + # Informative errors turned on so display the user to whom + # the ticket is assigned + printf( " (%s vs %s/%s)", + $t_user_name, $f_committer_name, $t_user_email ); + } + + printf( "'\r\n" ); + $t_all_ok = false; + } + } # End ownership must match ticket + + # Only allowed to commit against tickets with a specific status? + if( $t_repo_commit_status_restricted ) { + + # Check that the bug's status is at a level for which a commit + # is allowed + if( !in_array( $t_bug->status, $t_repo_commit_status_allowed )) { + + printf( "Check-Message: '%s : %s %d", + plugin_lang_get( 'error_commit_issue_wrong_status' ), + plugin_lang_get( 'issue' ), + $t_bug_id ); + + if( $t_informational_errors ) { + + # Informative errors turned on so display a list of statuses for which + # a commit would be accepted + + # Get an array of the names of the statuses for which commit is allowed + $t_statuses = array_map( function( $p_status ) { return get_enum_element( 'status', $p_status ); }, $t_repo_commit_status_allowed ); + + printf( " (%s vs %s)", + get_enum_element( 'status', $t_bug->status ), + implode( $t_statuses, ", " )); + } + printf( "'\r\n" ); + $t_all_ok = false; + } + } # End only allowed to commit against tickets with a specific status + + # Only allowed to commit against Mantis tickets within specific project(s) + if( $t_repo_commit_project_restricted ) { + + if( !in_array( 0, $t_repo_commit_project_allowed ) && + !in_array( $t_bug->project_id, $t_repo_commit_project_allowed )) { + + printf( "Check-Message: '%s : %s %d", + plugin_lang_get( 'error_commit_issue_wrong_project' ), + plugin_lang_get( 'issue' ), + $t_bug_id ); + + if( $t_informational_errors ) { + + # Informative errors turned on so display a list of Mantis projects to + # which referenced tickets must belong + + # Get an array of the names of all the projects + $t_projects = array_map( function( $p_proj ) { return project_get_field( $p_proj, 'name' ); }, $t_repo_commit_project_allowed ); + + printf( " (%s vs %s)", + project_get_field( $t_bug->project_id, 'name' ), + implode( $t_projects, ", " )); + } + + printf( "'\r\n" ); + $t_all_ok = false; + } + } # End only allowed to commit against tickets within specific projects + + # Committer must belong to the Mantis project? + if( $t_repo_commit_committer_must_be_member ) { + + $t_user_id = user_get_id_by_name( $f_committer_name ); + + # Didn't find the username? Try the e-mail address + if( $t_user_id == false ) { + + $t_user_id = user_get_id_by_email( $f_committer_name ); + } + + /* Check that the user exists in Mantis */ + if( $t_user_id == false ) { + + printf( "Check-Message: '%s : %s %d (%s)'\r\n", + plugin_lang_get( 'error_commit_committer_not_found' ), + plugin_lang_get( 'issue' ), + $t_bug_id, $f_committer_name ); + $t_all_ok = false; + + /* Check that the user is assigned to the project */ + } elseif( ! project_includes_user( $t_bug->project_id, $t_user_id )) { + printf( "Check-Message: '%s : %s %d (%s)'\r\n", + plugin_lang_get( 'error_commit_committer_not_member' ), + plugin_lang_get( 'issue' ), + $t_bug_id, $f_committer_name ); + $t_all_ok = false; + + } else { + + $t_user_access_level = project_get_local_user_access_level( $t_bug->project_id, $t_user_id ); + if( !in_array( $t_user_access_level, $t_repo_commit_committer_must_be_level )) { + + printf( "Check-Message: '%s : %s %d", + plugin_lang_get( 'error_commit_committer_wrong_level' ), + plugin_lang_get( 'issue' ), + $t_bug_id ); + + if( $t_informational_errors ) { + + # Informative errors turned on so display a list of access levels + # for which commit is allowed + + $t_levels = MantisEnum::getAssocArrayIndexedByValues( config_get( 'access_levels_enum_string' ) ); + $t_allowed_levels = array_intersect_key( $t_levels, array_flip( $t_repo_commit_committer_must_be_level )); + + printf( " (%s vs %s)", + $t_levels[ $t_user_access_level ], + implode( array_values( $t_allowed_levels ), ", " ) ); + } + printf( "'\r\n" ); + $t_all_ok = false; + } + } + } # End committer must belong to mantis project + } else { + + /* If the issue doesn't exist, then can't perform the checks */ + if( $t_repo_commit_issues_must_exist || + $t_repo_commit_ownership_must_match || + $t_repo_commit_status_restricted || + $t_repo_commit_project_restricted || + $t_repo_commit_committer_must_be_member ) { + + printf( "Check-Message: '%s : %s %d'\r\n", + plugin_lang_get( 'error_commit_nonexistent_issue' ), + plugin_lang_get( 'issue' ), + $t_bug_id ); + $t_all_ok = false; + } + } + } } -printf("Check-OK: %d\r\n",$t_all_ok ); +printf( "Check-OK: %d\r\n",$t_all_ok ); ?> diff --git a/Source/pages/repo_update_page.php b/Source/pages/repo_update_page.php index 3da36a212..047191615 100755 --- a/Source/pages/repo_update_page.php +++ b/Source/pages/repo_update_page.php @@ -23,8 +23,7 @@ html_page_top1( plugin_lang_get( 'title' ) ); html_page_top2(); -if( ON == config_get( 'use_javascript' ) ) -{ +if( ON == config_get( 'use_javascript' ) ) { $t_useJS = ' onclick="Source_Update_CheckOpts();" '; html_javascript_link( 'addLoadEvent.js' ); ?> @@ -128,7 +127,7 @@ function Source_Update_CheckOpts() id="repo_commit_project_restricted" name="repo_commit_project_restricted" type="checkbox" /> -> +> diff --git a/Source/pages/si_common.php b/Source/pages/si_common.php index bc79f8da3..111ce6bf5 100755 --- a/Source/pages/si_common.php +++ b/Source/pages/si_common.php @@ -1,8 +1,8 @@ From 76fd1d4800f6531f653b495f1e79af1db90f9a8a Mon Sep 17 00:00:00 2001 From: bright-tools Date: Sat, 3 Oct 2015 16:05:54 +0100 Subject: [PATCH 30/36] Suppress progress bar in commit output Add cmd line parameters to curl invokation in order to prevent the status report being shown, e.g.: % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 169 100 85 100 84 4856 4799 --:--:-- --:--:-- --:--:-- 5000 --- SourceSVN/pre-commit.tmpl.mantis-checks-commit | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SourceSVN/pre-commit.tmpl.mantis-checks-commit b/SourceSVN/pre-commit.tmpl.mantis-checks-commit index 4d76069d8..737d5fc93 100755 --- a/SourceSVN/pre-commit.tmpl.mantis-checks-commit +++ b/SourceSVN/pre-commit.tmpl.mantis-checks-commit @@ -46,8 +46,9 @@ echo 'commit_comment="' >> ${COMMENT_FILE} "${SVNLOOK}" log -t "${TXN}" "${REPOS}" >> ${COMMENT_FILE} echo '"' >> ${COMMENT_FILE} -# Fire off the query to Mantis -"${CURL}" -d "repo_name=${PROJECT}" -d "committer=${COMMITTING_USER}" -d @${COMMENT_FILE} -d "api_key=${API_KEY}" ${URL} >> ${LOG_FILE} +# Fire off the query to Mantis (-s -S to prevent progress bar but keep error +# reporting +"${CURL}" -s -S -d "repo_name=${PROJECT}" -d "committer=${COMMITTING_USER}" -d @${COMMENT_FILE} -d "api_key=${API_KEY}" ${URL} >> ${LOG_FILE} # Try and extract the result from the response RESULT=$(grep -oP 'Check-OK: \K([0-9]+)' ${LOG_FILE}) From aedbf6ab2363a04b8520162fa20f6be1c5805930 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Sun, 4 Oct 2015 11:39:12 +0100 Subject: [PATCH 31/36] Fixes #80 Add a check upon bug closure as to the user's permissions with respect to the project prior to assigning the bug to them --- Source/Source.API.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Source.API.php b/Source/Source.API.php index 60c6f88e5..cbb8fc41b 100644 --- a/Source/Source.API.php +++ b/Source/Source.API.php @@ -364,7 +364,7 @@ function Source_Process_Changesets( $p_changesets, $p_repo=null ) { } } - if ( $t_handler && !is_null( $t_user_id ) ) { + if ( $t_handler && !is_null( $t_user_id ) && access_has_project_level( config_get( 'handle_bug_threshold' ), $t_bug->project_id, $t_user_id )) { $t_bug->handler_id = $t_user_id; } From 70708703fd02572fa2d820bbc1ae6b409dd68007 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Sun, 4 Oct 2015 14:44:02 +0100 Subject: [PATCH 32/36] Initial test support via Vagrant Use Vagrant to set up a virtual machine, install Apache, MySQL, etc & then run some tests against the commit checking functionality --- CommitMessageCheck.md | 20 +++ testsupport/Vagrantfile | 97 +++++++++++++++ .../test_enabled_invalid_bug_ref_allowed.txt | 3 + ...st_enabled_invalid_bug_ref_not_allowed.txt | 5 + .../test_enabled_no_bug_ref.txt | 5 + .../expectedoutput/test_not_enabled.txt | 3 + testsupport/run_tests.sh | 114 ++++++++++++++++++ 7 files changed, 247 insertions(+) create mode 100755 testsupport/Vagrantfile create mode 100755 testsupport/expectedoutput/test_enabled_invalid_bug_ref_allowed.txt create mode 100755 testsupport/expectedoutput/test_enabled_invalid_bug_ref_not_allowed.txt create mode 100755 testsupport/expectedoutput/test_enabled_no_bug_ref.txt create mode 100755 testsupport/expectedoutput/test_not_enabled.txt create mode 100755 testsupport/run_tests.sh diff --git a/CommitMessageCheck.md b/CommitMessageCheck.md index 8223a660f..027cec517 100755 --- a/CommitMessageCheck.md +++ b/CommitMessageCheck.md @@ -124,3 +124,23 @@ That's unfortunate and will require some manual intervention. Possibly the easi The Mantis installation and the VCS repository need to be set up to share a private key (as per SourceIntegration). This means that someone without access to the API key will not be able to access the functionality used to check commit messages. In the case that someone has access to the API (either by having access to the private key or by having commit access to an associated repository) then negative responses are intended to be informational in order to assist the user in understanding why the commit was refused. This may mean that information relating to access levels or user names may be shown in the refusal message. If this is a concern, changing the value of `$t_informational_errors` from `true` to `false` in `Source/pages/pre_commit_check.php` will restrict the information to a minimum. + +## Testing + +The code can be tested in an automated manner thanks to +[Vagrant](https://www.vagrantup.com/). You need to have this installed in order +to run the tests + +In order to set up the appropriate environment: + + bright-tools@ubuntu:~/source-integration$ cd testsupport + bright-tools@ubuntu:~/source-integration/testsupport$ vagrant up + +This should create a virtual machine and install the appropriate software +(including Mantis & importing the SourceIntegration code from your working area). + +To run the tests: + + bright-tools@ubuntu:~/source-integration/testsupport$ vagrant ssh + ... + vagrant@vagrant-ubuntu-trusty-64:~$ /vagrant/run_tests.sh diff --git a/testsupport/Vagrantfile b/testsupport/Vagrantfile new file mode 100755 index 000000000..0e7be88b2 --- /dev/null +++ b/testsupport/Vagrantfile @@ -0,0 +1,97 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# All Vagrant configuration is done below. The "2" in Vagrant.configure +# configures the configuration version (we support older styles for +# backwards compatibility). Please don't change it unless you know what +# you're doing. +Vagrant.configure(2) do |config| + # The most common configuration options are documented and commented below. + # For a complete reference, please see the online documentation at + # https://docs.vagrantup.com. + + # Every Vagrant development environment requires a box. You can search for + # boxes at https://atlas.hashicorp.com/search. + config.vm.box = "ubuntu/trusty64" + + # Disable automatic box update checking. If you disable this, then + # boxes will only be checked for updates when the user runs + # `vagrant box outdated`. This is not recommended. + # config.vm.box_check_update = false + + # Create a forwarded port mapping which allows access to a specific port + # within the machine from a port on the host machine. In the example below, + # accessing "localhost:8080" will access port 80 on the guest machine. + config.vm.network "forwarded_port", guest: 80, host: 8080 + + # Create a private network, which allows host-only access to the machine + # using a specific IP. + # config.vm.network "private_network", ip: "192.168.33.10" + + # Create a public network, which generally matched to bridged network. + # Bridged networks make the machine appear as another physical device on + # your network. + # config.vm.network "public_network" + + # Share an additional folder to the guest VM. The first argument is + # the path on the host to the actual folder. The second argument is + # the path on the guest to mount the folder. And the optional third + # argument is a set of non-required options. + config.vm.synced_folder "../", "/source-integration" + + # Provider-specific configuration so you can fine-tune various + # backing providers for Vagrant. These expose provider-specific options. + # Example for VirtualBox: + # + config.vm.provider "virtualbox" do |vb| + # # Display the VirtualBox GUI when booting the machine + # vb.gui = true + # + # Customize the amount of memory on the VM - needed because MySQL fails + # install with the default 1/2 gig + vb.memory = "1024" + end + # + # View the documentation for the provider you are using for more + # information on available options. + + # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies + # such as FTP and Heroku are also available. See the documentation at + # https://docs.vagrantup.com/v2/push/atlas.html for more information. + # config.push.define "atlas" do |push| + # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" + # end + + # Enable provisioning with a shell script. Additional provisioners such as + # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the + # documentation for more information about their specific syntax and use. + config.vm.provision "shell", inline: <<-SHELL + sudo apt-get update + sudo apt-get install -y apache2 libapache2-mod-php5 subversion openssl + + # Automated MySQL installation + sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password password abc123' + sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password abc123' + sudo apt-get install -y mysql-server-5.6 php5-mysql + + # Install Mantis + sudo apt-get install unzip + cd /tmp + wget http://downloads.sourceforge.net/project/mantisbt/mantis-stable/1.2.19/mantisbt-1.2.19.zip + unzip mantisbt-1.2.19.zip + cp -r mantisbt-1.2.19 /var/www/html/mantis + + # Install Source Integration base + cp -r /source-integration/Source /var/www/html/mantis/plugins + cp -r /source-integration/SourceSVN /var/www/html/mantis/plugins + + # Fix permissions + sudo chown -R www-data:www-data /var/www/html/mantis + sudo find /var/www/html/mantis -type d -exec chmod 755 {} + + sudo chmod og+r -R /var/www/html/mantis + + # Configure Mantis + wget http://localhost/mantis/admin/install.php?db_type=mysql&hostname=localhost&username=root&db_password=abc123&database_name=bugtracker&install=2 + + SHELL +end diff --git a/testsupport/expectedoutput/test_enabled_invalid_bug_ref_allowed.txt b/testsupport/expectedoutput/test_enabled_invalid_bug_ref_allowed.txt new file mode 100755 index 000000000..dec1f1014 --- /dev/null +++ b/testsupport/expectedoutput/test_enabled_invalid_bug_ref_allowed.txt @@ -0,0 +1,3 @@ +Adding svn_sandbox/file2 +Transmitting file data . +Committed revision 2. diff --git a/testsupport/expectedoutput/test_enabled_invalid_bug_ref_not_allowed.txt b/testsupport/expectedoutput/test_enabled_invalid_bug_ref_not_allowed.txt new file mode 100755 index 000000000..82bf6728b --- /dev/null +++ b/testsupport/expectedoutput/test_enabled_invalid_bug_ref_not_allowed.txt @@ -0,0 +1,5 @@ +Adding svn_sandbox/file3 +Transmitting file data .svn: E165001: Commit failed (details follow): +svn: E165001: Commit blocked by pre-commit hook (exit code 1) with output: +Commit comment references non-existent issue : Issue 9999 + diff --git a/testsupport/expectedoutput/test_enabled_no_bug_ref.txt b/testsupport/expectedoutput/test_enabled_no_bug_ref.txt new file mode 100755 index 000000000..5031045ec --- /dev/null +++ b/testsupport/expectedoutput/test_enabled_no_bug_ref.txt @@ -0,0 +1,5 @@ +Adding svn_sandbox/file2 +Transmitting file data .svn: E165001: Commit failed (details follow): +svn: E165001: Commit blocked by pre-commit hook (exit code 1) with output: +Commit comments needs to reference one or more issues + diff --git a/testsupport/expectedoutput/test_not_enabled.txt b/testsupport/expectedoutput/test_not_enabled.txt new file mode 100755 index 000000000..2f6db4d72 --- /dev/null +++ b/testsupport/expectedoutput/test_not_enabled.txt @@ -0,0 +1,3 @@ +Adding svn_sandbox/file1 +Transmitting file data . +Committed revision 1. diff --git a/testsupport/run_tests.sh b/testsupport/run_tests.sh new file mode 100755 index 000000000..7b1f604ed --- /dev/null +++ b/testsupport/run_tests.sh @@ -0,0 +1,114 @@ +# Get a login +export WGET_PARAMS="--keep-session-cookies --save-cookies cookies.txt --load-cookies cookies.txt" +export REPO_NAME=test1 + +set_repo_options() +{ + wget -nv -O- $WGET_PARAMS --post-data="page=Source/repo_update_page&id=`cat repo_id.txt`" http://localhost/mantis/plugin.php | sed -ne 's/.*plugin_Source_repo_update_token" value="\([A-Za-z0-9]*\).*/\1/p' > update_token.txt + wget -nv -O- $WGET_PARAMS --post-data="page=Source/repo_update&repo_id=`cat repo_id.txt`&plugin_Source_repo_update_token=`cat update_token.txt`&repo_commit_issues_must_exist=$2&repo_commit_needs_issue=$1&repo_name=$REPO_NAME&repo_url=file%3A%2F%2F%2Fhome%2Fvagrant%2Fsvn_repo&svn_username=&svn_password=&standard_repository=&trunk_path=&branch_path=&tag_path=&ignore_paths=" http://localhost/mantis/plugin.php > /dev/null +} + +setup_mantis() +{ + wget -nv -O- --keep-session-cookies --save-cookies cookies.txt --post-data 'username=administrator&password=root&perm_login=1' http://localhost/mantis/login.php > /dev/null + # Install 'Source' + wget -nv -O- $WGET_PARAMS http://localhost/mantis/manage_plugin_page.php | sed -ne 's/.*\(manage_plugin_install\.php?name=Source[a-zA-Z0-9=&;_]*\).*/\1/p' | sed -e 's/\(&\)/\&/g' > install_url.txt + wget -nv -O- $WGET_PARAMS http://localhost/mantis/`cat install_url.txt` > /dev/null + # Install 'Subversion Integration' + wget -nv -O- $WGET_PARAMS http://localhost/mantis/manage_plugin_page.php | sed -ne 's/.*\(manage_plugin_install\.php?name=SourceSVN[a-zA-Z0-9=&;_]*\).*/\1/p' | sed -e 's/\(&\)/\&/g' > install_url.txt + wget -nv -O- $WGET_PARAMS http://localhost/mantis/`cat install_url.txt` > /dev/null +} + +configure_si() +{ + # Configure Source Integration + openssl rand -hex 12 > api_key.txt + wget -nv -O- $WGET_PARAMS http://localhost/mantis/plugin.php?page=Source/manage_config_page | sed -ne 's/.*plugin_Source_manage_config_token" value="\([A-Za-z0-9]*\).*/\1/p' > config_token.txt + wget -nv -O- $WGET_PARAMS --post-data="enable_message=1&import_urls=localhost&checkin_urls=localhost&bugfix_handler=1&bugfix_message_view_status=10&bugfix_message=Fix%20committed%20to%20\$1%20branch.&bugfix_resolution=20&bugfix_status=-1&bugfix_regex_2=/#?(\\d%2b)/&bugfix_regex_1=/(?:fixe?d?s?|resolved?s?)%2b\\s*:?\\s%2b(?:#(?:\\d%2b)[,\\.\\s]*)%2b/i&buglink_regex_2=/#?(\\d%2b)/&buglink_regex_1=/(?:bugs?|issues?|reports?)%2b\\s*:?\\s%2b(?:#(?:\\d%2b)[,\\.\\s]*)%2b/i&api_key=`cat api_key.txt`&show_repo_link=1&username_threshold=55&manage_threshold=90&update_threshold=20&view_threshold=10&plugin_Source_manage_config_token=`cat config_token.txt`" http://localhost/mantis/plugin.php?page=Source/manage_config > /dev/null +} + +setup_mantis_repo() +{ +# Set up a repo +wget -nv -O- $WGET_PARAMS http://localhost/mantis/plugin.php?page=Source/index | sed -ne 's/.*plugin_Source_repo_create_token" value="\([A-Za-z0-9]*\).*/\1/p' > create_token.txt +wget -nv -O- $WGET_PARAMS --post-data="repo_name=$REPO_NAME&repo_type=svn&plugin_Source_repo_create_token=`cat create_token.txt`" http://localhost/mantis/plugin.php?page=Source/repo_create > /dev/null +wget -nv -O- $WGET_PARAMS http://localhost/mantis/plugin.php?page=Source/index | grep -A6 $REPO_NAME | sed -ne 's/.*id=\([^"]*\).*/\1/p' > repo_id.txt +} + +delete_mantis_repo() +{ +# Set up a repo +wget -nv -O- $WGET_PARAMS --post-data="id=`cat repo_id.txt`" http://localhost/mantis/plugin.php?page=Source/repo_manage_page | sed -ne 's/.*plugin_Source_repo_delete_token" value="\([A-Za-z0-9]*\).*/\1/p' > delete_token.txt +wget -nv -O- $WGET_PARAMS --post-data="_confirmed=1&id=`cat repo_id.txt`&plugin_Source_repo_delete_token=`cat delete_token.txt`" http://localhost/mantis/plugin.php?page=Source/repo_delete > /dev/null +#rm repo_id.txt +} + +setup_project() +{ +# Set up a project +wget -nv -O- $WGET_PARAMS http://localhost/mantis/manage_proj_create_page.php | sed -ne 's/.*manage_proj_create_token" value="\([A-Za-z0-9]*\).*/\1/p' > proj_create_token.txt +wget -nv -O- $WGET_PARAMS --post-data="description=test&name=test_project&status=10&view_state=10&repo_type=svn&manage_proj_create_token=`cat proj_create_token.txt`" http://localhost/mantis/manage_proj_create.php > /dev/null +wget -nv -O- $WGET_PARAMS http://localhost/mantis/manage_proj_page.php | grep test_project | sed -ne 's/.*project_id=\([^"]\).*/\1/p' > project_id.txt +# Add a category to the project +wget -nv -O- $WGET_PARAMS http://localhost/mantis/manage_proj_edit_page.php?project_id=`cat project_id.txt` | sed -ne 's/.*manage_proj_cat_add_token" value="\([A-Za-z0-9]*\).*/\1/p' > cat_add_token.txt +wget -nv -O- $WGET_PARAMS --post-data="manage_proj_cat_add_token=`cat cat_add_token.txt`&project_id=`cat project_id.txt`&name=Cat1" http://localhost/mantis/manage_proj_cat_add.php > /dev/null +} + +check_test_result() +{ + cmp $1 /vagrant/expectedoutput/$1 > /dev/null 2>&1 + if [ $? == 0 ] + then + echo "Test $1: OK" + else + echo "Test $1: Failed" + fi +} + +setup_mantis +configure_si +setup_mantis_repo +setup_project + +rm -rf test_*.txt +rm -rf svn_repo +mkdir svn_repo +svnadmin create svn_repo +cat /var/www/html/mantis/plugins/SourceSVN/pre-commit.tmpl.mantis-checks-commit | sed -e "s/PROJECT=\"\([a-zA-Z0-9]*\)\"/PROJECT=\"$REPO_NAME\"/;s/API_KEY=.*/API_KEY=\"`cat api_key.txt`\"/" > svn_repo/hooks/pre-commit +cat /var/www/html/mantis/plugins/SourceSVN/post-commit.tmpl | sed -e "s/mantisbt/mantis/;s/PROJECT=\"\([a-zA-Z0-9 ]*\)\"/PROJECT=\"$REPO_NAME\"/;s/API_KEY=.*/API_KEY=\"`cat api_key.txt`\"/" > svn_repo/hooks/post-commit +chmod +x svn_repo/hooks/pre-commit svn_repo/hooks/post-commit + +rm -rf svn_sandbox +svn checkout file:///`pwd`/svn_repo svn_sandbox + +# Check that we can do a commit without any bug reference when the option is +# disabled +set_repo_options 0 0 +touch svn_sandbox/file1 +svn add svn_sandbox/file1 > /dev/null +svn commit -m "Hello" svn_sandbox/file1 > test_not_enabled.txt 2>&1 + +# Enable 'commit requires issue reference' and try and commit without +# referencing an issue +set_repo_options 1 0 +touch svn_sandbox/file2 +svn add svn_sandbox/file2 > /dev/null +svn commit -m "Hello" svn_sandbox/file2 > test_enabled_no_bug_ref.txt 2>&1 + +# Include a bug reference (it's invalid, but checking of validity is not +# enabled, so the commit should be accepted +svn commit -m "bug: #9999" svn_sandbox/file2 > test_enabled_invalid_bug_ref_allowed.txt 2>&1 + +# Turn on checking of bug reference validity & try and commit again - +# should be rejected +set_repo_options 1 1 +touch svn_sandbox/file3 +svn add svn_sandbox/file3 > /dev/null +svn commit -m "bug: #9999" svn_sandbox/file3 > test_enabled_invalid_bug_ref_not_allowed.txt 2>&1 + +delete_mantis_repo + +for X in test_*.txt; do + check_test_result $X; +done + From 800e9e90208ed66313e964194f15bdd125b48342 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Sun, 4 Oct 2015 15:38:22 +0100 Subject: [PATCH 33/36] Improve error message when bug not assigned --- Source/pages/pre_commit_check.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Source/pages/pre_commit_check.php b/Source/pages/pre_commit_check.php index 3ec148eb7..39c71e4de 100755 --- a/Source/pages/pre_commit_check.php +++ b/Source/pages/pre_commit_check.php @@ -62,8 +62,13 @@ # Ownership of ticket must match committer? if( $t_repo_commit_ownership_must_match ) { - $t_user_name = user_get_name( $t_bug->handler_id ); - $t_user_email = user_get_email( $t_bug->handler_id ); + if( 0 == $t_bug->handler_id ) { + $t_user_name = 'none'; + $t_user_email = 'none'; + } else { + $t_user_name = user_get_name( $t_bug->handler_id ); + $t_user_email = user_get_email( $t_bug->handler_id ); + } # Check that the username of the committer matches the user name # or e-mail address of the owner of the ticket @@ -80,8 +85,8 @@ # Informative errors turned on so display the user to whom # the ticket is assigned - printf( " (%s vs %s/%s)", - $t_user_name, $f_committer_name, $t_user_email ); + printf( " (%s/%s vs %s)", + $t_user_name, $t_user_email, $f_committer_name ); } printf( "'\r\n" ); From 164e333aba167105ee5097797a7480ba550882aa Mon Sep 17 00:00:00 2001 From: bright-tools Date: Sat, 24 Oct 2015 11:12:47 +0100 Subject: [PATCH 34/36] Initial version of plugin to support linking from changesets to ViewVC --- SourceViewVC/LICENSE | 23 ++++++ SourceViewVC/SourceViewVC.php | 147 ++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100755 SourceViewVC/LICENSE create mode 100755 SourceViewVC/SourceViewVC.php diff --git a/SourceViewVC/LICENSE b/SourceViewVC/LICENSE new file mode 100755 index 000000000..9c6c2e6fb --- /dev/null +++ b/SourceViewVC/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2015 John Bailey + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + diff --git a/SourceViewVC/SourceViewVC.php b/SourceViewVC/SourceViewVC.php new file mode 100755 index 000000000..d3be20510 --- /dev/null +++ b/SourceViewVC/SourceViewVC.php @@ -0,0 +1,147 @@ +name = lang_get( 'plugin_SourceViewVC_title' ); + $this->description = lang_get( 'plugin_SourceViewVC_description' ); + + $this->version = '0.1'; + $this->requires = array( + 'MantisCore' => '1.2.0', + 'Source' => '0.16', + 'SourceSVN' => '0.16', + ); + + $this->author = 'John Bailey'; + $this->contact = 'dev@brightsilence.com'; + $this->url = 'https://github.com/bright-tools/source-integration'; + } + + public $type = 'viewvc'; + + public function show_type() { + return lang_get( 'plugin_SourceViewVC_svn' ); + } + + public function get_viewvc_url( $p_repo ) { + return isset( $p_repo->info['viewvc_url'] ) + ? $p_repo->info['viewvc_url'] + : ''; + } + + public function get_viewvc_name( $p_repo ) { + return isset( $p_repo->info['viewvc_name'] ) + ? $p_repo->info['viewvc_name'] + : ''; + } + + public function get_viewvc_use_checkout( $p_repo ) { + return isset( $p_repo->info['viewvc_use_checkout'] ) + ? $p_repo->info['viewvc_use_checkout'] + : false; + } + + /** + * Builds the ViewVC URL base string + * @param object $p_repo repository + * @param string $p_file optional filename (as absolute path from root) + * @param array $p_opts optional additional ViewVC URL parameters + * @return string ViewVC URL + */ + protected function url_base( $p_repo, $p_file = '', $p_opts=array() ) { + $t_name = urlencode( $this->get_viewvc_name( $p_repo ) ); + + $t_url = $this->get_viewvc_url( $p_repo ); + + return $t_url . '/'. $t_name . $p_file . '?' . http_build_query( $p_opts ); + } + + public function url_repo( $p_repo, $p_changeset=null ) { + $t_opts = array(); + + if ( !is_null( $p_changeset ) ) { + $t_opts['revision'] = $p_changeset->revision; + } + + return $this->url_base( $p_repo, '', $t_opts); + } + + public function url_changeset( $p_repo, $p_changeset ) { + $t_rev = $p_changeset->revision; + $t_opts = array(); + $t_opts['view'] = 'revision'; + $t_opts['revision'] = $t_rev; + + return $this->url_base( $p_repo, '', $t_opts ); + } + + public function url_file( $p_repo, $p_changeset, $p_file ) { + + # if the file has been removed, it doesn't exist in current revision + # so we generate a link to (current revision - 1) + $t_revision = ($p_file->action == 'rm') + ? $p_changeset->revision - 1 + : $p_changeset->revision; + $t_use_checkout = $this->get_viewvc_use_checkout( $p_repo ); + + $t_opts = array(); + $t_opts['revision'] = $t_revision; + if( !$t_use_checkout ) + { + $t_opts['view'] = 'markup'; + } + + return $this->url_base( $p_repo, $p_file->filename, $t_opts ); + } + + public function url_diff( $p_repo, $p_changeset, $p_file ) { + if ( $p_file->action == 'rm' || $p_file->action == 'add' ) { + return ''; + } + + $t_opts = array(); + $t_opts['r1'] = $p_changeset->revision; + $t_opts['r2'] = $p_changeset->revision - 1; + + return $this->url_base( $p_repo, $p_file->filename, $t_opts ); + } + + public function update_repo_form( $p_repo ) { + $t_url = $this->get_viewvc_url( $p_repo ); + $t_name = $this->get_viewvc_name( $p_repo ); + $t_use_checkout = $this->get_viewvc_use_checkout( $p_repo ); + +?> +> + + + +> + + + +> + +/> + +info['viewvc_url'] = gpc_get_string( 'viewvc_url' ); + $p_repo->info['viewvc_name'] = gpc_get_string( 'viewvc_name' ); + $p_repo->info['viewvc_use_checkout'] = gpc_get_bool( 'viewvc_use_checkout', false ); + + return parent::update_repo( $p_repo ); + } +} From 86136c91532954941e58ba15ade87a5b8212909d Mon Sep 17 00:00:00 2001 From: bright-tools Date: Fri, 30 Oct 2015 19:24:02 +0000 Subject: [PATCH 35/36] Strings file to support new plugin --- SourceViewVC/lang/strings_english.txt | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100755 SourceViewVC/lang/strings_english.txt diff --git a/SourceViewVC/lang/strings_english.txt b/SourceViewVC/lang/strings_english.txt new file mode 100755 index 000000000..65317ec30 --- /dev/null +++ b/SourceViewVC/lang/strings_english.txt @@ -0,0 +1,21 @@ +(With trailing slash)'; +$s_plugin_SourceViewVC_viewvc_name = 'ViewVC Name
(Repository directory)'; +$s_plugin_SourceViewVC_viewvc_use_checkout = 'ViewVC Checkout View Enabled?'; + +$s_plugin_SourceViewVC_svn_username = 'SVN Username'; +$s_plugin_SourceViewVC_svn_password = 'SVN Password'; +$s_plugin_SourceViewVC_standard_repo = 'Standard Repository
(trunk/branches/tags)'; +$s_plugin_SourceViewVC_trunk_path = 'Trunk Path
(Non-standard repository)'; +$s_plugin_SourceViewVC_branch_path = 'Branch Path
(Non-standard repository)'; +$s_plugin_SourceViewVC_tag_path = 'Tag Path
(Non-standard repository)'; +$s_plugin_SourceViewVC_ignore_paths = 'Ignore Other Paths
(Non-standard repository)'; From 5949e36cb7ec7b3cab8e28f5255fa1e03867f9f0 Mon Sep 17 00:00:00 2001 From: bright-tools Date: Sun, 1 Nov 2015 11:05:06 +0000 Subject: [PATCH 36/36] Add option to support the root_as_url_component configuration option in ViewVC --- SourceViewVC/SourceViewVC.php | 38 +++++++++++++++++++++------ SourceViewVC/lang/strings_english.txt | 1 + 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/SourceViewVC/SourceViewVC.php b/SourceViewVC/SourceViewVC.php index d3be20510..cf329ea52 100755 --- a/SourceViewVC/SourceViewVC.php +++ b/SourceViewVC/SourceViewVC.php @@ -42,13 +42,19 @@ public function get_viewvc_name( $p_repo ) { ? $p_repo->info['viewvc_name'] : ''; } - - public function get_viewvc_use_checkout( $p_repo ) { + + public function get_viewvc_use_checkout( $p_repo ) { return isset( $p_repo->info['viewvc_use_checkout'] ) ? $p_repo->info['viewvc_use_checkout'] : false; } + public function get_viewvc_root_as_url( $p_repo ) { + return isset( $p_repo->info['viewvc_root_as_url'] ) + ? $p_repo->info['viewvc_root_as_url'] + : false; + } + /** * Builds the ViewVC URL base string * @param object $p_repo repository @@ -58,10 +64,18 @@ public function get_viewvc_use_checkout( $p_repo ) { */ protected function url_base( $p_repo, $p_file = '', $p_opts=array() ) { $t_name = urlencode( $this->get_viewvc_name( $p_repo ) ); + $t_root_as_url = $this->get_viewvc_root_as_url( $p_repo ); + + $t_url = rtrim( $this->get_viewvc_url( $p_repo ), '/' ); - $t_url = $this->get_viewvc_url( $p_repo ); + if( $t_root_as_url ) { + $t_url_name = '/'.$t_name; + } else { + $t_url_name = ''; + $p_opts['root']=$t_name; + } - return $t_url . '/'. $t_name . $p_file . '?' . http_build_query( $p_opts ); + return $t_url . $t_url_name . $p_file . '?' . http_build_query( $p_opts ); } public function url_repo( $p_repo, $p_changeset=null ) { @@ -90,14 +104,15 @@ public function url_file( $p_repo, $p_changeset, $p_file ) { $t_revision = ($p_file->action == 'rm') ? $p_changeset->revision - 1 : $p_changeset->revision; - $t_use_checkout = $this->get_viewvc_use_checkout( $p_repo ); + $t_use_checkout = $this->get_viewvc_use_checkout( $p_repo ); $t_opts = array(); $t_opts['revision'] = $t_revision; - if( !$t_use_checkout ) - { + + if( !$t_use_checkout ) + { $t_opts['view'] = 'markup'; - } + } return $this->url_base( $p_repo, $p_file->filename, $t_opts ); } @@ -118,6 +133,7 @@ public function update_repo_form( $p_repo ) { $t_url = $this->get_viewvc_url( $p_repo ); $t_name = $this->get_viewvc_name( $p_repo ); $t_use_checkout = $this->get_viewvc_use_checkout( $p_repo ); + $t_root_as_url = $this->get_viewvc_root_as_url( $p_repo ); ?> > @@ -129,6 +145,10 @@ public function update_repo_form( $p_repo ) { > + +/> + +> /> @@ -138,9 +158,11 @@ public function update_repo_form( $p_repo ) { } public function update_repo( $p_repo ) { + $p_repo->info['viewvc_url'] = gpc_get_string( 'viewvc_url' ); $p_repo->info['viewvc_name'] = gpc_get_string( 'viewvc_name' ); $p_repo->info['viewvc_use_checkout'] = gpc_get_bool( 'viewvc_use_checkout', false ); + $p_repo->info['viewvc_root_as_url'] = gpc_get_bool( 'viewvc_root_as_url', false ); return parent::update_repo( $p_repo ); } diff --git a/SourceViewVC/lang/strings_english.txt b/SourceViewVC/lang/strings_english.txt index 65317ec30..ce47090f4 100755 --- a/SourceViewVC/lang/strings_english.txt +++ b/SourceViewVC/lang/strings_english.txt @@ -10,6 +10,7 @@ $s_plugin_SourceViewVC_description = 'Adds Subversion integration to the Source $s_plugin_SourceViewVC_svn = 'ViewVC'; $s_plugin_SourceViewVC_viewvc_url = 'ViewVC URL
(With trailing slash)'; $s_plugin_SourceViewVC_viewvc_name = 'ViewVC Name
(Repository directory)'; +$s_plugin_SourceViewVC_viewvc_root_as_url = 'ViewVC Root As URL Component Enabled?'; $s_plugin_SourceViewVC_viewvc_use_checkout = 'ViewVC Checkout View Enabled?'; $s_plugin_SourceViewVC_svn_username = 'SVN Username';