Friday, August 22, 2008

SVN-JIRA Pre-Commit Hook

Earlier I had posted a VBScript  version of the same integration Hook. This time I have come up with the PHP version. I know there is already a plugin available for JIRA, which does exactly the same thing. However that plugin needs to be installed on both the svn server and the JIRA server. It adds new methods to the JIRA web service.

The PHP version and the VBScript version which I presented earlier tends to be much more simpler, in that both of these hooks needs to be installed on svn server only and configuring them is a matter of changing one file, the hook script it-self. Here is the PHP code of the Hook script.

Linux Installation

Simply create a file named "pre-commit" in "hooks" directory of your svn repository and paste the following code in it. Change the values for following constants.

If you have multiple svn repositories then you will have to copy this file into those many directories.

  • JIRA_SERVER - This is the address of the JIRA web service. You need to change the IP address and port.
  • SVN_USER - The id of the JIRA user. This user must have the read access to all projects defined in JIRA and have corresponding svn repository for which the control needs to be enforced.
  • SVN_PASSWORD - The password for the JIRA user.
  • SVNLOOK_PATH - The full path to the "svnlook" executable.

After saving do not forget to change the permissions of the file. (chmod 755 pre-commit).

Pre-requisites

  • Subversion 1.4.6 or higher.
  • PHP 5.2.6 or higher.
    • PHP soap extension.
  • JIRA 3.x
    • JIRA rpc plugin enabled.

Source Code

  1: #!/usr/bin/php
  2: <?php
  3: //------------------------------------------------------------------------------
  4: // A php schell script to authorize commit only if it is accompanied by a log
  5: // message in <code>^([A-Z]*)\-(\d*)\s?\-\s?(.*)</code> format and that the issue
  6: // id present in the log message is valid issue (Open, In Progress or Re-Opened)
  7: // in JIRA and is assigned to the committer.
  8: //
  9: // e.g. ABC-1345 - Changed the implementation of login metod.
 10: //
 11: // AUTHOR : Prasad P. Khandekar
 12: // CREATED: August 22, 2008
 13: // VERSION: 1.0
 14: //------------------------------------------------------------------------------
 15: define("JIRA_SERVER", "http://<JIRA HOST>:<PORT>/JIRA/rpc/soap/jirasoapservice-v2?wsdl");
 16: define("SVN_USER", "ENTER JIRA USER");
 17: define("SVN_PASS", "ENTER PASSOWRD");
 18: define("SVNLOOK_PATH", "/usr/bin/svnlook");
 19: define("LOGMSG_REGX", "/^([A-Z]*)\-(\d*)\s?\-\s?(.*)/");
 20: 
 21: if ($argc < 3)
 22:     trigger_error("Insufficient command line arguments!", E_USER_ERROR);
 23: 
 24: $repoPath = $argv[1];
 25: $txnId = $argv[2];
 26: 
 27: $cmd = SVNLOOK_PATH . " info -r " . $txnId . " " . $repoPath;
 28: //$cmd = SVNLOOK_PATH . " info -t " .$txnId . " " . $repoPath;
 29: 
 30: $result = runCommand($cmd);
 31: if (!is_array($result))
 32:     trigger_error("Unable to retrieve commit info for commit transaction " .
 33:                     $txnId . " repository " . $repoPath, E_USER_ERROR);
 34: if (count($result) < 4)
 35:     trigger_error("Log message not supplied for commit transaction " . $txnId .
 36:                     " repository " . $repoPath, E_USER_ERROR);
 37: 
 38: $author = $result[0];
 39: $line = NULL;
 40: $issueId = NULL;
 41: $matches = NULL;
 42: for ($cntr = 3; $cntr < count($result); $cntr++)
 43: {
 44:     if (is_array($matches))
 45:         unset($matches);
 46: 
 47:     $line = $result[$cntr];
 48:     if (preg_match(LOGMSG_REGX, $line, $matches))
 49:     {
 50:         $issueId = $matches[1] . "-" . $matches[2];
 51:         break 1;
 52:     }
 53: }
 54: if (!isset($issueId) || is_null($issueId))
 55:     trigger_error("Unable to retrieve JIRA issue id for commit transaction " .
 56:                    $txnId, E_USER_ERROR);
 57: 
 58: if (!acceptCommit($author, $issueId))
 59:     trigger_error($author . " is not allowed to commit against issue (" .
 60:                     $issueId . ")!", E_USER_ERROR);
 61: else
 62:     exit(0);
 63: 
 64: //------------------------------------------------------------------------------
 65: // HELPER Functions
 66: //------------------------------------------------------------------------------
 67: /**
 68:  * Function acceptCommit calls the JIRA web service and checks following conditions
 69:  * 1. The Issue id represents a valid JIRA issue with one of the following status
 70:  *     1. Open
 71:  *     2. In Progress
 72:  *     3. Re-Opened
 73:  * 2. The issue is assigned to the committer.
 74:  *
 75:  * If both of these conditions are met then the commit is allowed.
 76:  * @param string - the user id of the committer.
 77:  * @param string - The JIRA issue id used in the commit log message.

 78: 
 79:  * @return boolean
 80:  */
 81: function acceptCommit($pAuthor, $pIssueId)
 82: {
 83:     $client = new SoapClient(JIRA_SERVER);
 84:     $authId = $client->login(SVN_USER, SVN_PASS);
 85:     if (is_soap_fault($authId))
 86:         trigger_error("JIRA Authentication failed!", E_USER_ERROR);
 87: 
 88:     $issue = $client->getIssue($authId, $pIssueId);
 89:     if (is_soap_fault($issue))
 90:         trigger_error("Usnable to retrieve details for issue " . $issueId,
 91:                         E_USER_ERROR);
 92: 
 93:     $status = intval($issue->status);
 94:     $author = $issue->assignee;
 95:     if (($status == 1 || $status == 3 || $status == 5) &&
 96:             strcasecmp($author, $pAuthor))
 97:         return true;
 98: 
 99:     return false;
100: }
101: 
102: /**
103:  * Function runCommand is used to launch a external program and capture it's
104:  * output.
105:  * @param string The shell command to be launched.
106:  * @param boolean A flag which allows running shell commands which do not return
107:  * anything.
108:  * @return array
109:  */
110: function runCommand($cmd, $mayReturnNothing = false)
111: {
112:     $output = array ();
113:     $err = false;
114: 
115:     $c = quoteCommand($cmd);
116: 
117:     $descriptorspec = array (0 => array('pipe', 'r'),
118:                             1 => array('pipe', 'w'),
119:                             2 => array('pipe', 'w'));
120: 
121:     $resource = proc_open($c, $descriptorspec, $pipes);
122:     $error = "";
123: 
124:     if (!is_resource($resource))
125:         trigger_error("BADCMD : " . $cmd, E_USER_ERROR);
126: 
127:     $handle = $pipes[1];
128:     $firstline = true;
129:  while (!feof($handle))
130:  {
131:         $line = trim(fgets($handle));
132:         if ($firstline && empty($line) && !$mayReturnNothing)
133:         {
134:             $err = true;
135:         }
136: 
137:         $firstline = false;
138:         if (!empty($line))
139:             $output[] = $line;
140:     }
141: 
142:     while (!feof($pipes[2]))
143:     {
144:         $error .= fgets($pipes[2]);
145:     }
146: 
147:     $error = trim($error);
148: 
149:     fclose($pipes[0]);
150:     fclose($pipes[1]);
151:     fclose($pipes[2]);
152: 
153:     proc_close($resource);
154: 
155:  if (!$err)
156:         return $output;
157:  else
158:         trigger_error($error, E_USER_ERROR);
159: }
160: 
161: /**
162:  * On windows you are required to enclose the command in double quotes to
163:  * avoid the problems that may occure due to long file or path names.
164:  * @param string The command to be enclosed in quotes.
165:  * @return string
166: 
167:  */
168: function quoteCommand($cmd)
169: {
170:     $osName = strtoupper(php_uname('s'));
171:     // On Windows machines, the whole line needs quotes round it so that it's
172:     // passed to cmd.exe correctly
173:     if (stripos($osName, "WINDOWS") !== false)
174:         $cmd = "\"$cmd\"";
175: 
176:     return $cmd;
177: }
178: ?>
179: 

1 comment:

Ofer said...

pre-requisites:
don't you have to have Apache installed to run the PHP?

Disclaimer: The views expressed in this blog are my own and do not necessarily reflect the views of any former, current or future employers or employees of mine