├── README └── git-jira-hook /README: -------------------------------------------------------------------------------- 1 | ======================================================================= 2 | Copyright 2009 Broadcom Corporation 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | ======================================================================== 16 | 17 | 18 | git-jira-hook README 19 | ******************** 20 | 21 | Author: Joyjit Nath 22 | ******************* 23 | 24 | 25 | Table of Contents 26 | ================= 27 | 1. Introduction 28 | 2. System Requirements 29 | 3. Installation 30 | 4. Using git-jira-hook 31 | 5. Known limitations 32 | 6. Frequently Asked Questions (FAQ) 33 | 7. Credits 34 | 35 | 36 | 1. Introduction 37 | =============== 38 | 39 | 1.1 Get git and Jira to work in Harmony 40 | --------------------------------------- 41 | If you are using git [1] for source control and Jira [2] for bug 42 | tracking, then the git-jira-hook script might be useful to you. 43 | 44 | Once you have the script setup in your environment, every time you 45 | make a git commit, a comment is automatically posted to an open 46 | issue in Jira. 47 | 48 | This script will also "enforce" that for every git commit, you have at 49 | least one Jira issue that you are referencing. 50 | 51 | This way you have a paper trail of the history behind each and every 52 | git commit. 53 | 54 | This is particularly useful for corporate git repositories. 55 | 56 | 57 | 1.2 Example use 58 | --------------- 59 | In order to specify which issue (or issues) you want the commit message 60 | to get tracked to in Jira, you place magic text markers such as: 61 | 62 | "refs #NNN" or "fixes #NNN" 63 | 64 | anywhere in your commit message, where "NNN" is the name of an open 65 | Jira issue. 66 | 67 | For example, say you have typed in the following commit message in 68 | your git repository. 69 | 70 | Hey look! This is my very first git-commit 71 | 72 | Using the new and fresh git-jira-hook 73 | 74 | refs #SW-189, refs #HW-278 fixes #FW-702 75 | 76 | 77 | The following things will happen: 78 | 79 | In your Jira bug database, for projects named "TST", "HW" and "FW" 80 | in Jira, for issue numbers "SW-189", "HW-278", and "FW-702", the 81 | following comments will be added: 82 | 83 | commit 424daa955f5c8a17aab9d524071f65f1999769a9 84 | Author: Joyjit Nath 85 | Date: Tue Aug 25 15:12:39 2009 -0700 86 | 87 | Hey look! This is my very first git-commit 88 | 89 | Using the new and fresh git-jira-hook 90 | 91 | refs #SW-189, refs #HW-278 fixes #FW-702 92 | 93 | In addition, 94 | * the issue "FW=702" will be marked as resolved. (NOTE: 95 | this "resolved" part does not work yet. See "Known 96 | Limitations" section.) 97 | 98 | 1.3 Wait! there's more 99 | ----------------------- 100 | 101 | If your git repository is exposed using gitweb [3], an hyperlink 102 | linking to the exact commit will also be embedded in the Jira issue 103 | comment that was added. This enables anyone to examine your git 104 | commit simply by clicking on the hyperlink. 105 | 106 | 2. System Requirements 107 | ====================== 108 | - Python 2.x and python modules: SOAPpy, ConfigParser. 109 | I have tested with Python 2.5.2. 110 | 111 | - A Jira installation with Remote APIs enabled. 112 | 113 | - git version 1.6.x.y (I have tested with 1.6.0.4). 114 | 115 | - Linux or some other similar Unix flavor (I have tested with 116 | CentOS 5.x). 117 | 118 | - OPTIONAL, but Highly recommended: gitweb [4] which has been 119 | setup with "upstream" git repositories. 120 | 121 | 122 | 123 | 3. Installation 124 | =============== 125 | Here is a typical example of how this hook may be used. 126 | 127 | In a corporate setting where git is used, there is typically an 128 | "upstream" or "public" repository. And then developers have their 129 | "private" repositories. For their day-to-day work, the developers 130 | use their private repositories. Periodically, they (either 131 | directly, or via gatekeepers) push changes from their private 132 | repository to the "upstream" one. Also, the "upstream" repository 133 | is typically a bare repository, and no actual commits are done 134 | here. 135 | 136 | In the private repository, the hook should be installed, but only 137 | *partically*. Every time a commit is made, the hook only checks to 138 | make sure that the commit text conforms to correct formatting (i.e. 139 | the magic references to Jira issues are present). No Jira issues 140 | are updated. 141 | 142 | The hook is completely installed in the "upstream" repository. 143 | 144 | Whenever a commit is made in the upstream repository or a "git push" 145 | is done to it, the installed hook will kick in and validate the 146 | commit message, followed by update of the Jira issue. 147 | 148 | 149 | 3.1 Installtion for "private" repository 150 | ---------------------------------------- 151 | (i) Copy this script to /hooks/commit-msg 152 | and mark it executable 153 | Example: 154 | cp git-jira-hook joyjit-project/.git/hooks/commit-msg 155 | chmod +x joyjit-project/.git/hooks/commit-msg 156 | 157 | 158 | (iii) Set the following git config value 159 | Example: 160 | cd joyjit-repo 161 | git config jira.url "http://jira.mycompany.com" 162 | 163 | (iv) [Optional] If you wish jira integration to be triggered only 164 | on certain branches, add a comma-separated list of 165 | branch names to git config "git-jira-hook.branches" 166 | For example: 167 | 168 | cd joyjit-repo 169 | git config git-jira-hook.branches "jira1,jira2" 170 | 171 | This will cause the integration to be triggered only on 172 | git branches jira1 and jira2. For instance, if you make a commit 173 | to branch "master", the hook will simply stay disabled. 174 | 175 | By default, if you do not set this config, all branches are 176 | checked. 177 | 178 | See the "Frequently Asked Questions" section to figure out what 179 | values to use for your "jira.url". 180 | 181 | 3.2 Installation for "upstream" repository 182 | ------------------------------------------ 183 | (i) Copy this script to 184 | /hooks/{commit-msg|post-commit|update|post-receive} 185 | and mark it executable 186 | 187 | Example: 188 | cp git-jira-hook upstream-project.git/hooks/commit-msg 189 | cp git-jira-hook upstream-project.git/hooks/post-commit 190 | cp git-jira-hook upstream-project.git/hooks/update 191 | cp git-jira-hook upstream-project.git/hooks/post-receive 192 | chmod +x upstream-project.git/hooks/commit-msg 193 | chmod +x upstream-project.git/hooks/post-commit 194 | chmod +x upstream-project.git/hooks/update 195 | chmod +x upstream-project.git/hooks/post-receive 196 | 197 | (iii) Set the following git config values (Note: gitweb.url config is 198 | recommended, but Optional): "jira.url" "gitweb.url" 199 | Example: 200 | cd upstream.git 201 | git config jira.url "http://jira.mycompany.com" 202 | git config gitweb.url "http://git.mycompany.com/gitweb.cgi/p=upstream-project.git;a=commit;h=" 203 | 204 | (iv) [Optional] If you wish jira integration to be triggered only 205 | on certain branches, add a comma-separated list of 206 | branch names to git config "git-jira-hook.branches" 207 | For example: 208 | 209 | cd joyjit-repo 210 | git config git-jira-hook.branches "jira1,jira2" 211 | 212 | This will cause the integration to be triggered only on 213 | git branches jira1 and jira2. For instance, if you make a commit 214 | to branch "master", the hook will simply stay disabled. 215 | 216 | By default, if you do not set this config, all branches are 217 | checked. 218 | See the "Frequently Asked Questions" section to figure out what values 219 | to use for your "jira.url" and "gitweb.url" 220 | 221 | 222 | 4. Using git-jira-hook 223 | ====================== 224 | When you are read to make a git commit, make sure that you have an 225 | appropriate open jira issue. There can be more than one open issues. 226 | Lets say this commit deals with Jira issues FOO-23 and BAR-42 and 227 | also marks FOO-56 as resolved. 228 | 229 | Anywhere in your commit message, you must put the following strings 230 | (without the quotes): 231 | "refs #FOO-23" 232 | "refs #BAR-42" 233 | "fixes #FOO-56" 234 | 235 | 236 | 237 | And then you do a "git commit" the normal way. At this time, assuming 238 | the "private" repository, the commit message will be checked for 239 | references to Jira issues and the commit will succeed only if these 240 | issues exist. 241 | 242 | And then, at a later time, when you do "git push" to push your changes 243 | upstream, the final validation and Jira issue update will be done. 244 | 245 | NOTE: The "fixes" feature does not work yet :< 246 | 247 | 248 | 5. Known Limitations 249 | ==================== 250 | 251 | I am working to fix all of these issues: 252 | * The "fixes" text does not yet mark the Jira issue as resolved. 253 | 254 | * The error messages are a bit confusing (cluttered with too much detail). 255 | 256 | DISCLAIMER: This is my very first attempt at writing python code, so it is not 257 | very well written. 258 | 259 | 260 | 6. Frequently asked Questions 261 | ============================= 262 | 263 | Q 6.1 What value should I use for "jira.url" git config? 264 | A. It depends on your Jira server setup. When you log-in to the Jira 265 | server using a browser, the URL to the login page typically looks like: 266 | http://jira.mycompany.com/secure/Dashboard.jspa 267 | In which case, your "jira.url" should be "http://jira.mycompany.com" 268 | 269 | 270 | Q 6.2 What value should I use for "gitweb.url" git config? 271 | A. Assuming you have gitweb enabled for your repository, this is the URL which you 272 | use to access gitweb. 273 | for instance, in order to view commit "424daa955f5c8a17aab9d524071f65f1999769a9" 274 | in gitweb, if you use: 275 | http://git.mycompany.com/gitweb.cgi?p=joyjit-repo/.git;a=commit;h=424daa955f5c8a17aab9d524071f65f1999769a9 276 | 277 | Then the "gitweb.url" to use is: 278 | "http://git.mycompany.com/gitweb.cgi?p=joyjit-repo/.git;a=commit;h=" 279 | 280 | 281 | 7. Credits 282 | ========== 283 | This script was inspired by the following: 284 | http://github.com/dreiss/git-jira-attacher/tree/master 285 | http://confluence.atlassian.com/display/JIRAEXT/Jira+CLI 286 | 287 | 288 | 8. References 289 | ============= 290 | [1] Git, an source configuratiin management ("SCM") tool 291 | http://git-scm.com/ 292 | 293 | [2] Jira, a bug tracking system 294 | http://www.atlassian.com/software/jira 295 | 296 | [3] Gitweb, a web based browser for git 297 | http://git.or.cz/gitwiki/Gitweb 298 | -------------------------------------------------------------------------------- /git-jira-hook: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ########################################################################## 4 | # Copyright 2009 Broadcom Corporation 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | # File: git-jira-hook 19 | # Author: Joyjit Nath 20 | # 21 | ########################################################################### 22 | 23 | # Purpose: 24 | # This is a git hook, to be used in an environment where git is used 25 | # as the source control and Jira is used for bug tracking. 26 | # 27 | # See accompanying README file for help in using this. 28 | # 29 | 30 | 31 | 32 | from __future__ import with_statement 33 | 34 | import logging 35 | import sys 36 | import os 37 | 38 | myname = os.path.basename(sys.argv[0]) 39 | 40 | # Change this value to "CRITICAL/ERROR/WARNING/INFO/DEBUG/NOTSET" 41 | # as appropriate. 42 | # loglevel=logging.INFO 43 | # loglevel=logging.DEBUG 44 | 45 | 46 | import contextlib 47 | import subprocess 48 | import re 49 | import collections 50 | import getpass 51 | import SOAPpy 52 | import traceback 53 | import pprint 54 | import pdb 55 | import stat 56 | import cookielib 57 | import subprocess 58 | import urllib2 59 | import ConfigParser 60 | import string 61 | 62 | 63 | def main(): 64 | global myname, loglevel 65 | logging.basicConfig(level=loglevel, format=myname + ":%(levelname)s: %(message)s") 66 | 67 | if myname == "commit-msg" : 68 | return handle_commit_msg() 69 | 70 | elif myname == "post-commit" : 71 | return handle_post_commit() 72 | 73 | elif myname == "update": 74 | return handle_update() 75 | 76 | elif myname == "post-receive": 77 | return handle_post_receive() 78 | 79 | 80 | else: 81 | logging.error("invoked as '%s'. Need to be invoked as commit-msg, post-commit, update or post-receive" , myname) 82 | return -1 83 | 84 | 85 | # This function performs the git "commit-msg" hook 86 | # In this function, the commit message text is parsed for the magic 87 | # text referencing Jira issues. 88 | # It logs into Jira and makes sure that the Jira issues exist 89 | # and are accessible 90 | # This function does not actually add any comments to Jira 91 | # as the user's commit has not yet gone through (that 92 | # Returns: 0 on success, -1 on error 93 | def handle_commit_msg(): 94 | if not enabled_on_branch(git_get_curr_branchname()): 95 | return 0 96 | 97 | if len(sys.argv) < 2 : 98 | logging.error("No commit message filename specified") 99 | return -1 100 | 101 | commit_msg_filename = sys.argv[1] 102 | 103 | jira_url = get_jira_url() 104 | if jira_url == None: 105 | return -1 106 | 107 | try: 108 | mode = os.stat(commit_msg_filename)[stat.ST_MODE] 109 | if not stat.S_ISREG(mode): 110 | logging.error("'%s' is not a valid file", commit_msg_filename) 111 | 112 | except KeyboardInterrupt: 113 | logging.info('... interrupted') 114 | 115 | except Exception, e: 116 | logging.error("Failed to open file '%s'", commit_msg_filename) 117 | logging.debug(e) 118 | return -1 119 | 120 | (jira_soap_client, jira_auth) = jira_start_session(jira_url) 121 | 122 | if jira_soap_client == None or jira_auth == None: 123 | return -1 124 | 125 | try: 126 | commit_msg_text = open(commit_msg_filename).read() 127 | 128 | except KeyboardInterrupt: 129 | logging.info('... interrupted') 130 | 131 | except Exception, e: 132 | logging.error("Failed to open file '%s'", commit_msg_filename) 133 | logging.debug(e) 134 | return -1 135 | 136 | return validate_commit_text(jira_soap_client, jira_auth, commit_msg_text) 137 | 138 | 139 | # Performs the git "post-commit" hook 140 | # Uses "git" to find out the most recent commit 141 | # Parses commit message text to find references to Jira issues 142 | # and updates the Jira issue by adding the commit message text 143 | 144 | # to the issue 145 | # Returns: Nothing (as returning error wont do us any good 146 | # on a post-commit hook) 147 | def handle_post_commit(): 148 | if not enabled_on_branch(git_get_curr_branchname()): 149 | return 0 150 | 151 | jira_url = get_jira_url() 152 | if jira_url == None: 153 | return 154 | 155 | commit_id = git_get_last_commit_id() 156 | commit_text = git_get_commit_msg(commit_id) 157 | 158 | (jira_soap_client, jira_auth) = jira_start_session(jira_url) 159 | 160 | jira_add_comment(jira_soap_client, jira_auth, commit_id, commit_text) 161 | return 162 | 163 | 164 | # Performs the git "update" hook 165 | # This hook is triggered on the remote repo, as a result 166 | # of "git push" 167 | # Parses the old and new commit IDs from argv[2] and argv[3] 168 | # argv[1] contains the "refname" 169 | def handle_update(): 170 | if len(sys.argv) < 4: 171 | logging.error("update hook called with incorrect no. of parameters") 172 | return -1 173 | 174 | ref = sys.argv[1] # This is of the form "refs/heads/" 175 | old_commit_id = sys.argv[2] 176 | new_commit_id = sys.argv[3] 177 | 178 | if not enabled_on_branch(git_get_branchname_from_ref(ref)): 179 | return 0 180 | 181 | jira_url = get_jira_url() 182 | if jira_url == None: 183 | return -1 184 | 185 | (jira_soap_client, jira_auth) = jira_start_session(jira_url) 186 | if jira_soap_client == None or jira_auth == None: 187 | return -1 188 | 189 | commit_id_array = git_get_array_of_commit_ids(old_commit_id, new_commit_id) 190 | 191 | for commit_id in commit_id_array: 192 | commit_text = git_get_commit_msg(commit_id) 193 | if validate_commit_text(jira_soap_client, jira_auth, commit_text, commit_id) != 0: 194 | return -1 195 | 196 | return 0 197 | 198 | # post-receive hook is called with no parameters 199 | # but STDIN has 200 | def handle_post_receive(): 201 | buf = sys.stdin.read() 202 | logging.debug("handle_post_receive: stdin='%s'", buf) 203 | (old_commit_id, new_commit_id, ref) = string.split(buf, ' ') 204 | 205 | 206 | if old_commit_id == None or new_commit_id == None or ref == None: 207 | logging.error("post-receive hook stdin is incorrect '%s'", buf) 208 | return -1 209 | 210 | if not enabled_on_branch(git_get_branchname_from_ref(ref)): 211 | return 0 212 | 213 | 214 | jira_url = get_jira_url() 215 | if jira_url == None: 216 | return -1 217 | 218 | (jira_soap_client, jira_auth) = jira_start_session(jira_url) 219 | if jira_soap_client == None or jira_auth == None: 220 | return -1 221 | 222 | commit_id_array = git_get_array_of_commit_ids(old_commit_id, new_commit_id) 223 | 224 | if commit_id_array == None or len(commit_id_array)==0: 225 | logging.error("no commit ids!") 226 | return -1 227 | 228 | for commit_id in commit_id_array: 229 | commit_text = git_get_commit_msg(commit_id) 230 | jira_add_comment(jira_soap_client, jira_auth, commit_id, commit_text) 231 | 232 | return 0 233 | 234 | 235 | 236 | def validate_commit_text(jira_soap_client, jira_auth, commit_text, commit_id=None): 237 | refed_issue_count = call_pattern_hook(commit_text, "refs", \ 238 | jira_find_issue, jira_soap_client, jira_auth, None) 239 | 240 | if refed_issue_count == -1: 241 | return -1 242 | 243 | fixed_issue_count = call_pattern_hook(commit_text, "fixes", \ 244 | jira_find_issue, jira_soap_client, jira_auth, None) 245 | 246 | if fixed_issue_count == -1: 247 | return -1 248 | 249 | 250 | if refed_issue_count + fixed_issue_count == 0: 251 | if commit_id != None: 252 | logging.error("Failed to find any referenced Jira issue\n\tin commit message for commit %s", commit_id) 253 | else: 254 | logging.error("Failed to find any referenced Jira issue in commit message(s)") 255 | return -1 256 | 257 | return 0 258 | 259 | 260 | def jira_add_comment(jira_soap_client, jira_auth, commit_id, commit_text): 261 | gitweb_url = get_gitweb_url() 262 | if gitweb_url != None or gitweb_url != "": 263 | commit_text_with_url = commit_text.replace(commit_id, \ 264 | "[" + commit_id + "|" + gitweb_url + commit_id + "]") 265 | else: 266 | commit_text_with_url = commit_text 267 | 268 | 269 | call_pattern_hook(commit_text, 'refs', jira_add_comment_to_issue, \ 270 | jira_soap_client, jira_auth, commit_text_with_url) 271 | call_pattern_hook(commit_text, 'fixes', jira_add_comment_to_and_fix_issue, \ 272 | jira_soap_client, jira_auth, commit_text_with_url) 273 | return 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | # Given a function pointer, iterates through the commit message 282 | # text for Jira magic words, and calls the function repeatedly 283 | # returns number of issues found and touched 284 | # in case of error, return -1 285 | def call_pattern_hook(text, pattern, hookfn, jira_soap_client, jira_auth, jira_text): 286 | if not callable(hookfn): 287 | logging.error("Hook function is not callable"); 288 | exit -1 289 | 290 | magic = re.compile(pattern + ' #\w\w*-\d\d*') 291 | 292 | iterator = magic.finditer(text) 293 | issue_count = 0 294 | for match in iterator: 295 | issuekey = match.group().split(" ", 2)[1].strip('#') 296 | # print "issuekey found=", issuekey 297 | ret = hookfn(issuekey, jira_soap_client, jira_auth, jira_text) 298 | if ret != 0: 299 | return -1 300 | else: 301 | issue_count += 1 302 | 303 | return issue_count 304 | 305 | #----------------------------------------------------------------------------- 306 | # Jira helper functions 307 | # 308 | 309 | 310 | # Given a Jira server URL (which is stored in git config) 311 | # Starts an authenticated jira session using SOAP api 312 | # Returns a list of the SOAP object and the authentication token 313 | def jira_start_session(jira_url): 314 | jira_url = jira_url.rstrip("/") 315 | try: 316 | handle = urllib2.urlopen(jira_url + "/rpc/soap/jirasoapservice-v2?wsdl") 317 | soap_client = SOAPpy.WSDL.Proxy(handle) 318 | # print "self.soap_client set", self.soap_client 319 | 320 | except KeyboardInterrupt: 321 | logging.info("... interrupted") 322 | 323 | except Exception, e: 324 | save_jira_cached_auth(jira_url, "") 325 | logging.error("Invalid Jira URL: '%s'", jira_url) 326 | logging.debug(e) 327 | return -1 328 | 329 | auth = jira_login(jira_url, soap_client) 330 | if auth == None: 331 | return (None, None) 332 | 333 | return (soap_client, auth) 334 | 335 | # Try to use the cached authentication object to log in 336 | # to Jira first. ("implicit") 337 | # if that fails, then prompt the user ("explicit") 338 | # for username/password 339 | def jira_login(jira_url, soap_client): 340 | 341 | auth = get_jira_cached_auth(jira_url) 342 | if auth != None and auth != "": 343 | auth = jira_implicit_login(soap_client, auth) 344 | else: 345 | auth = None 346 | 347 | if auth == None: 348 | save_jira_cached_auth(jira_url, "") 349 | auth = jira_explicit_login(soap_client) 350 | 351 | 352 | if auth != None: 353 | save_jira_cached_auth(jira_url, auth) 354 | 355 | return auth 356 | 357 | def jira_implicit_login(soap_client, auth): 358 | 359 | # test jira to see if auth is valid 360 | try: 361 | jira_types = soap_client.getIssueTypes(auth) 362 | return auth 363 | except KeyboardInterrupt: 364 | logging.info("... interrupted") 365 | 366 | except Exception, e: 367 | print >> sys.stderr, "Previous Jira login is invalid or has expired" 368 | # logging.debug(e) 369 | 370 | 371 | return None 372 | 373 | def jira_explicit_login(soap_client): 374 | max_retry_count = 3 375 | retry_count = 0 376 | 377 | while retry_count < max_retry_count: 378 | if retry_count > 0: 379 | logging.info("Invalid Jira password/username combination, try again") 380 | 381 | # We now need to read the Jira username/password from 382 | # the console. 383 | # However, there is a problem. When git hooks are invoked 384 | # stdin is pointed to /dev/null, see here: 385 | # http://kerneltrap.org/index.php?q=mailarchive/git/2008/3/4/1062624/thread 386 | # The work-around is to re-assign stdin back to /dev/tty , as per 387 | # http://mail.python.org/pipermail/patches/2002-February/007193.html 388 | sys.stdin = open('/dev/tty', 'r') 389 | 390 | username = raw_input('Jira username: ') 391 | password = getpass.getpass('Jira password: ') 392 | 393 | # print "abc" 394 | # print "self.soap_client login...%s " % username + password 395 | try: 396 | auth = soap_client.login(username, password) 397 | 398 | try: 399 | jira_types = soap_client.getIssueTypes(auth) 400 | return auth 401 | 402 | except KeyboardInterrupt: 403 | logging.info("... interrupted") 404 | 405 | except Exception,e: 406 | logging.error("User '%s' does not have access to Jira issues") 407 | return None 408 | 409 | except KeyboardInterrupt: 410 | logging.info("... interrupted") 411 | 412 | except Exception,e: 413 | logging.debug("Login failed") 414 | 415 | auth=None 416 | retry_count = retry_count + 1 417 | 418 | 419 | if auth == None: 420 | logging.error("Invalid Jira password/username combination") 421 | 422 | return auth 423 | 424 | 425 | 426 | def jira_find_issue(issuekey, jira_soap_client, jira_auth, jira_text): 427 | try: 428 | issue = jira_soap_client.getIssue(jira_auth, issuekey) 429 | logging.debug("Found issue '%s' in Jira: (%s)", 430 | issuekey, issue["summary"]) 431 | return 0 432 | 433 | except KeyboardInterrupt: 434 | logging.info("... interrupted") 435 | 436 | except Exception, e: 437 | logging.error("No such issue '%s' in Jira", issuekey) 438 | logging.debug(e) 439 | return -1 440 | 441 | 442 | def jira_add_comment_to_issue(issuekey, jira_soap_client, jira_auth, jira_text): 443 | try: 444 | jira_soap_client.addComment(jira_auth, issuekey, {"body":jira_text}) 445 | logging.debug("Added to issue '%s' in Jira:\n%s", issuekey, jira_text) 446 | 447 | except Exception, e: 448 | logging.error("Error adding comment to issue '%s' in Jira", issuekey) 449 | logging.debug(e) 450 | return -1 451 | 452 | 453 | # TODO: Not fully implemented yet! 454 | def jira_add_comment_to_and_fix_issue(issuekey, jira_soap_client, jira_text): 455 | return jira_add_comment_to_issue(issuekey, jira_soap_client, jira_text) 456 | 457 | 458 | 459 | 460 | #----------------------------------------------------------------------------- 461 | # Miscellaneous Jira related utility functions 462 | # 463 | def get_jira_url(): 464 | jira_url = git_config_get("jira.url") 465 | if jira_url == None or jira_url == "": 466 | logging.error("Jira URL is not set. Please use 'git config jira.url to set it'") 467 | return None 468 | 469 | return jira_url 470 | 471 | def get_jira_cached_auth(jira_url): 472 | return get_cfg_value(os.environ['HOME'] + "/.jirarc", jira_url, "auth") 473 | 474 | def save_jira_cached_auth(jira_url, auth): 475 | return save_cfg_value(os.environ['HOME'] + "/.jirarc", jira_url, "auth", auth) 476 | 477 | 478 | #--------------------------------------------------------------------- 479 | # Misc. helper functions 480 | # 481 | def get_gitweb_url(): 482 | return git_config_get("gitweb.url") 483 | 484 | def get_cfg_value(cfg_file_name, section, key): 485 | try: 486 | cfg = ConfigParser.ConfigParser() 487 | cfg.read(cfg_file_name) 488 | value = cfg.get(section, key) 489 | except: 490 | return None 491 | return value 492 | 493 | 494 | def save_cfg_value(cfg_file_name, section, key, value): 495 | try: 496 | cfg = ConfigParser.SafeConfigParser() 497 | except Exception, e: 498 | logging.warning("Failed to instantiate a ConfigParser object") 499 | logging.debug(e) 500 | return 501 | 502 | try: 503 | cfg.read(cfg_file_name) 504 | except Exception, e: 505 | logging.warning("Failed to read .jirarc") 506 | logging.debug(e) 507 | return 508 | 509 | try: 510 | cfg.add_section(section) 511 | except ConfigParser.DuplicateSectionError,e: 512 | logging.debug("Section '%s' already exists in '%s'", section, cfg_file_name) 513 | 514 | try: 515 | cfg.set(section, key, value) 516 | except Exception,e: 517 | logging.warning("Failed to add '%s' to '%s'", key, cfg_file_name) 518 | logging.debug(e) 519 | 520 | try: 521 | cfg.write(open(cfg_file_name, 'wb')) 522 | except Exception, e: 523 | logging.warning("Failed to write '%s'='%s' to file %s", key, value, cfg_file_name) 524 | logging.debug(e) 525 | return 526 | 527 | # given a string, executes it as an executable, and returns the STDOUT 528 | # as a string 529 | def get_shell_cmd_output(cmd): 530 | try: 531 | proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) 532 | return proc.stdout.read().rstrip('\n') 533 | except KeyboardInterrupt: 534 | logging.info("... interrupted") 535 | 536 | except Exception, e: 537 | logging.error("Failed trying to execute '%s'", cmd) 538 | 539 | #---------------------------------------------------------------------------- 540 | # git helper functions 541 | # 542 | 543 | # Read git config of "git-jira-hook.branches" 544 | # Parse out the comma (and space) separated list of 545 | # branch names. 546 | # Then compare against current branchname to see 547 | # if we need to be enabled. 548 | # Return False if we should not be enabled 549 | def enabled_on_branch(current_branchname): 550 | logging.debug("Test if '%s' is enabled...", current_branchname) 551 | branchstr = git_config_get("git-jira-hook.branches") 552 | if branchstr == None or string.strip(branchstr) == "": 553 | logging.debug("All branches enabled") 554 | return not False 555 | 556 | branchlist = string.split(branchstr, ',') 557 | 558 | for branch in branchlist: 559 | branch = string.strip(branch) 560 | if current_branchname == branch: 561 | logging.debug("Current branch '%s' is enabled", current_branchname) 562 | return not False 563 | 564 | logging.debug("Curent branch '%s' is NOT enabled", current_branchname) 565 | return False 566 | 567 | # Get our current branchname 568 | def git_get_curr_branchname(): 569 | buf = get_shell_cmd_output("git branch --no-color") 570 | # buf is a multiline output, each line containing a branch name 571 | # the line that starts with a "*" contains the current branch name 572 | 573 | m = re.search("^\* .*$", buf, re.MULTILINE) 574 | if m == None: 575 | return None 576 | 577 | return buf[m.start()+2 : m.end()] 578 | 579 | 580 | # Given a "ref" string (such as while doing a push 581 | # to a remote repo), parse out the branch name 582 | def git_get_branchname_from_ref(ref): 583 | # "refs/heads/" 584 | if string.find(ref, "refs/heads") != 0: 585 | logging.error("Invalid ref '%s'", ref) 586 | exit -1 587 | 588 | return string.strip(ref[len("refs/heads/"):]) 589 | 590 | 591 | def git_config_get(name): 592 | return get_shell_cmd_output("git config '" + name + "'") 593 | 594 | def git_config_set(name, value): 595 | os.system("git config " + name + " '" + value + "'") 596 | 597 | def git_config_unset(name): 598 | os.system("git config --unset-all " + name) 599 | 600 | def git_get_commit_msg(commit_id): 601 | return get_shell_cmd_output("git rev-list --pretty --max-count=1 " + commit_id) 602 | 603 | def git_get_last_commit_id(): 604 | return get_shell_cmd_output("git log --pretty=format:%H -1") 605 | 606 | def git_get_array_of_commit_ids(start_id, end_id): 607 | output = get_shell_cmd_output("git rev-list " + start_id + ".." + end_id) 608 | if output == "": 609 | return None 610 | 611 | # parse the result into an array of strings 612 | commit_id_array = string.split(output, '\n') 613 | return commit_id_array 614 | 615 | 616 | #---------------------------------------------------------------------------- 617 | # python script entry point. Dispatches main() 618 | if __name__ == "__main__": 619 | exit (main()) 620 | 621 | --------------------------------------------------------------------------------