├── .gitignore ├── LICENSE ├── README.md ├── git-checkout-task └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | /test-repo -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Daniil Popov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-checkout-task 2 | [![Latest release](https://img.shields.io/github/release/int02h/git-checkout-task.svg)](https://github.com/int02h/git-checkout-task/releases/latest) 3 | 4 | Git custom command to checkout branch for a specific task 5 | 6 | ## Description 7 | 8 | This custom git command is intended to simplify checkout of feature-branch for a specific task from issue tracker like Jira. 9 | 10 | ## Requirements 11 | 12 | - Feature-branch must contain a task ID anywhere within it. The following branchs names are valid for task `ABC-1234`: `ABC-1234-awesome-bugfix`, `ABC-1234/awesome-bugfix`, `feature/ABC-1234-awesome-feature`, and etc. 13 | 14 | ## How to use 15 | 16 | ```bash 17 | git checkout-task ABS-1234 18 | ``` 19 | 20 | If the command name looks long for you just add an alias for it: 21 | 22 | ```bash 23 | git config --global alias.cot checkout-task 24 | ``` 25 | 26 | ## How it works 27 | 28 | 1. Grep all branches including remote ones with the task ID 29 | 1. Removes everything before task id thus `remotes/origin/ABC-1234-awesome-bugfix` becomes just `ABC-1234-awesome-bugfix` or `remotes/origin/ABC-1234/awesome-bugfix` becomes `ABC-1234/awesome-bugfix` 30 | 1. Checkout that branch 31 | 32 | ## Edge cases 33 | 34 | **C** - case, **B** - behavior 35 | 36 | **C**: You're already on the desired branch 37 | **B**: No branch checkout. Info message printed 38 | 39 | **C**: There are no branches with the specified prefix 40 | **B**: No branch checkout. Error message printed 41 | 42 | **C**: There is more than one branch with the specified prefix 43 | **B**: No branch checkout. The names of clashing branches will be printed 44 | 45 | ## How to install 46 | 47 | Put the file `git-checkout-task` to any folder that is in the PATH and make it executable. 48 | 49 | For example: 50 | 51 | ```bash 52 | cd ~ && \ 53 | mkdir .gitbin && \ 54 | cd .gitbin && \ 55 | curl -o git-checkout-task https://raw.githubusercontent.com/int02h/git-checkout-task/master/git-checkout-task && \ 56 | chmod +x git-checkout-task 57 | ``` 58 | 59 | Then add `~/.gitbin` to the PATH. Put the following line in the `~/.bashrc`, or `~/.zshrc`, or in the script for whatever shell you're using: 60 | 61 | ```bash 62 | export PATH=$PATH:~/.gitbin 63 | ``` 64 | 65 | ## Changelog 66 | 67 | ### v1.0.1 68 | 69 | - Support slash `/` in branch name so both `ABC-1234-awesome-bugfix` and `ABC-1234/awesome-bugfix` are valid 70 | 71 | ### v1.0.2 72 | 73 | - Task ID can be anywhere within a branch name, not only at the beginning. 74 | 75 | ## License 76 | 77 | Copyright (c) 2023 Daniil Popov 78 | 79 | Licensed under the [MIT](LICENSE) License. 80 | -------------------------------------------------------------------------------- /git-checkout-task: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright (c) 2023 Daniil Popov 4 | # Licensed under the MIT License. 5 | 6 | # Version 1.0.2 7 | # https://github.com/int02h/git-checkout-task 8 | 9 | set -e 10 | 11 | readonly task_id=$1 12 | 13 | readonly currentBranch=$(git rev-parse --abbrev-ref HEAD) 14 | if [[ $currentBranch == *$task_id* ]] 15 | then 16 | echo "Already on $currentBranch" 17 | exit 0 18 | fi 19 | 20 | readonly localBranches=( $(git branch --format="%(refname:lstrip=2)" | grep "$task_id") ) 21 | readonly remoteBranches=( $(git branch -r --format="%(refname:lstrip=3)" | grep "$task_id") ) 22 | readonly branches=(`for R in "${localBranches[@]}" "${remoteBranches[@]}" ; do echo "$R" ; done | sort -du`) 23 | 24 | 25 | if [ ${#branches[@]} -eq 0 ] 26 | then 27 | echo "No branches found for $task_id" 28 | exit 1 29 | fi 30 | 31 | if [ ${#branches[@]} -eq 1 ] 32 | then 33 | echo "Checking out ${branches[0]}" 34 | git checkout ${branches[0]} 35 | exit 0 36 | fi 37 | 38 | echo "There are clashing branches for $task_id:" 39 | printf -- "- %s\n" "${branches[@]}" 40 | exit 1 -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import shutil 4 | import tempfile 5 | import unittest 6 | import subprocess 7 | 8 | class Git: 9 | 10 | repo = Path("./test-repo").resolve() 11 | 12 | def init(self): 13 | shutil.rmtree(self.repo, ignore_errors=True) 14 | self.repo.mkdir() 15 | os.chdir(self.repo) 16 | os.system("git init") 17 | os.system("touch file.txt") 18 | os.system("git add file.txt") 19 | os.system("git commit -m \"test commit\"") 20 | return self 21 | 22 | def createBranch(self, name): 23 | os.system("git branch %s" % name) 24 | 25 | def deleteBranchLocally(self, name): 26 | os.system("git branch -D %s" % name) 27 | 28 | def checkoutBranch(self, name): 29 | os.system("git checkout %s" % name) 30 | 31 | def checkoutTask(self, id): 32 | result = subprocess.getoutput("git checkout-task %s" % id).strip() 33 | print("checkoutTask: %s" % result) 34 | return result 35 | 36 | def setupRemote(self): 37 | os.system("git remote add test-origin git@github.com:int02h/git-checkout-task.git") 38 | os.system("git fetch") 39 | return self 40 | 41 | def setupUpstream(self, branch): 42 | os.system("git branch -u test-origin/%s %s" % (branch, branch)) 43 | 44 | class TestGitCommand(unittest.TestCase): 45 | 46 | def test_no_branch(self): 47 | git = Git().init() 48 | self.assertEqual(git.checkoutTask("ABC-123"), "No branches found for ABC-123") 49 | 50 | def test_already_on_branch(self): 51 | git = Git().init() 52 | git.createBranch("ABC-123/test-branch") 53 | git.checkoutBranch("ABC-123/test-branch") 54 | self.assertEqual(git.checkoutTask("ABC-123"), "Already on ABC-123/test-branch") 55 | 56 | def test_more_than_one_branch(self): 57 | git = Git().init() 58 | git.createBranch("ABC-123/test-branch-1") 59 | git.createBranch("ABC-123/test-branch-2") 60 | self.assertEqual(git.checkoutTask("ABC-123"), "There are clashing branches for ABC-123:\n- ABC-123/test-branch-1\n- ABC-123/test-branch-2") 61 | 62 | 63 | def test_checkout_success(self): 64 | git = Git() 65 | git.init() 66 | git.createBranch("ABC-123/test-branch") 67 | self.assertTrue(git.checkoutTask("ABC-123").startswith("Checking out ABC-123/test-branch")) 68 | 69 | git.init() 70 | git.createBranch("ABC-123-test-branch") 71 | self.assertTrue(git.checkoutTask("ABC-123").startswith("Checking out ABC-123-test-branch")) 72 | 73 | git.init() 74 | git.createBranch("feature/ABC-123-test-branch") 75 | self.assertTrue(git.checkoutTask("ABC-123").startswith("Checking out feature/ABC-123-test-branch")) 76 | 77 | git.init() 78 | git.createBranch("bug-fix/ABC-123-test-branch") 79 | self.assertTrue(git.checkoutTask("ABC-123").startswith("Checking out bug-fix/ABC-123-test-branch")) 80 | 81 | def test_checkout_success_remote_and_local(self): 82 | git = Git().init().setupRemote() 83 | git.createBranch("ABC-123-test-branch") 84 | git.setupUpstream("ABC-123-test-branch") 85 | self.assertTrue(git.checkoutTask("ABC-123").startswith("Checking out ABC-123-test-branch")) 86 | 87 | def test_checkout_success_remote_only(self): 88 | git = Git().init().setupRemote() 89 | git.createBranch("ABC-123-test-branch") 90 | git.setupUpstream("ABC-123-test-branch") 91 | git.deleteBranchLocally("ABC-123-test-branch") 92 | self.assertTrue(git.checkoutTask("ABC-123").startswith("Checking out ABC-123-test-branch")) 93 | 94 | if __name__ == '__main__': 95 | unittest.main() --------------------------------------------------------------------------------