├── requirements.txt ├── clean_up_history.json ├── .github └── workflows │ └── pylint.yml ├── color_logger.py ├── helper.py ├── .gitignore ├── Readme.md └── main.py /requirements.txt: -------------------------------------------------------------------------------- 1 | pylint==2.11.1 2 | -------------------------------------------------------------------------------- /clean_up_history.json: -------------------------------------------------------------------------------- 1 | { 2 | "processed": [], 3 | "deleted": [], 4 | "tagged": [] 5 | } -------------------------------------------------------------------------------- /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 3.9 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: 3.9 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install pylint 20 | - name: Analysing the code with pylint 21 | run: | 22 | pylint `ls -R|grep .py$|xargs` 23 | -------------------------------------------------------------------------------- /color_logger.py: -------------------------------------------------------------------------------- 1 | """A Custom formatter for python Logger""" 2 | import logging 3 | 4 | 5 | class CustomFormatter(logging.Formatter): 6 | """Adds different colors to Log_level, Time and date to each log message.""" 7 | grey = "\x1b[38;21m" 8 | yellow = "\x1b[33;21m" 9 | red = "\x1b[31;21m" 10 | bold_red = "\x1b[31;1m" 11 | reset = "\x1b[0m" 12 | log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)" 13 | 14 | FORMATS = { 15 | logging.DEBUG: grey + log_format + reset, 16 | logging.INFO: grey + log_format + reset, 17 | logging.WARNING: yellow + log_format + reset, 18 | logging.ERROR: red + log_format + reset, 19 | logging.CRITICAL: bold_red + log_format + reset 20 | } 21 | 22 | def format(self, record): 23 | """Formats the record""" 24 | log_fmt = self.FORMATS.get(record.levelno) 25 | formatter = logging.Formatter(log_fmt) 26 | return formatter.format(record) 27 | -------------------------------------------------------------------------------- /helper.py: -------------------------------------------------------------------------------- 1 | """Created by Lingaraj Sankaravelu at 08:16 PM 17-10-21 using PyCharm""" 2 | 3 | from json import load, dump 4 | from pathlib import Path 5 | 6 | 7 | class CleanUpHistoryHelper: 8 | """Used to record the tagged names and deleted branch names to clean_up_history.json""" 9 | 10 | KEY_PROCESSED = 'processed' 11 | KEY_DELETED = 'deleted' 12 | KEY_TAGGED = 'tagged' 13 | 14 | def __init__(self): 15 | # Reads the data from the json file, named clean_up_history.json 16 | base_path = Path(__file__).parent 17 | self.file_path = (base_path / "clean_up_history.json").resolve() 18 | with open(self.file_path, 'r', encoding='utf-8') as file_object: 19 | self.json = load(file_object) 20 | # parse different key values from cleanup status map 21 | self.processed = self.json[self.KEY_PROCESSED] 22 | self.deleted = self.json[self.KEY_DELETED] 23 | self.tagged = self.json[self.KEY_TAGGED] 24 | 25 | def is_deleted(self, branch_name): 26 | """return True if branch name is deleted already false otherwise""" 27 | return branch_name in self.deleted 28 | 29 | def is_tagged(self, tag_name): 30 | """return True if branch name is tagged already false otherwise""" 31 | return tag_name in self.tagged 32 | 33 | def is_processed(self, branch_name): 34 | """return True if branch name is processed already false otherwise""" 35 | return branch_name in self.processed 36 | 37 | def mark_deleted(self, name): 38 | """Append the branchName to processed and deleted, also write the same to file""" 39 | if name not in self.processed: 40 | self.processed.append(name) 41 | if name not in self.deleted: 42 | self.deleted.append(name) 43 | self.write_to_file__() 44 | 45 | def mark_tagged(self, branch_name, tag_name): 46 | """Records the branch_name as processed,deleted and tag_name to tagged""" 47 | self.processed.append(branch_name) 48 | self.deleted.append(branch_name) 49 | self.tagged.append(tag_name) 50 | self.write_to_file__() 51 | 52 | def write_to_file__(self): 53 | """Writes the local processed, Deleted, Tagged array to json file""" 54 | self.json[self.KEY_PROCESSED] = self.processed 55 | self.json[self.KEY_DELETED] = self.deleted 56 | self.json[self.KEY_TAGGED] = self.tagged 57 | with open(self.file_path, 'w', encoding='utf-8') as outfile: 58 | dump(self.json, outfile, indent=4) 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | #pycharm 141 | .idea -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # BranchToTag 2 | ### Python based script to create git tags for the given branch names. 3 | Recently I had to come up with an easy way to cleanup a Git repository with some 200+ branches, Where we also wanted to archive few branches as tags. 4 | 5 | ## Features 6 | - Create git Tags and delete the tagged branches. 7 | - Delete the given branches without tagging. 8 | - Maintain the history of the script process under clean_up_history.json 9 | 10 | ## How To Use (Changes required in main.py) 11 | 12 | 1. Set your local repo path 13 | ``` 14 | LOCAL_REPO_PATH = "/Users/PycharmProjects/your_local_project_git_folder/" 15 | ``` 16 | 17 | 2. If you want to create git tags and delete the tagged branches. 18 | 19 | ``` 20 | if __name__ == '__main__': 21 | set_local_repo() 22 | branches = ['branch_name_1', branch_name_2 ...] 23 | create_tags(branches)```` 24 | ``` 25 | 26 | 3. If you want to just delete some git branches 27 | 28 | ``` 29 | if __name__ == '__main__': 30 | set_local_repo() 31 | branches = ['branch_name_1', branch_name_2 ...] 32 | delete_branches(branches)``` 33 | ``` 34 | 35 | 4. If you wish to change the prefix of the tag modify this. 36 | 37 | ``` 38 | TAG_PREFIX = "archive" 39 | ``` 40 | 41 | ## Run 42 | main.py 43 | 44 | ## clean_up_history.json 45 | Either the script is creating tag or deleting a given a branch. It will be recorded in the clean_up_history.json for future reference. 46 | ```yaml 47 | { 48 | //name of the branch which are already processed by the script 49 | "processed": [], 50 | // name of the branch which are deleted 51 | "deleted": [], 52 | //name of the tags created by the script 53 | "tagged": [] 54 | } 55 | ``` 56 | 57 | ## Contributing 58 | Love your input! I wanted to make contributing to this project as easy and transparent as possible, whether it's: 59 | - Reporting a bug 60 | - Discussing the current state of the code 61 | - Submitting a fix 62 | 63 | ## License 64 | ``` 65 | MIT License 66 | 67 | Copyright (c) 2021 Lingaraj Sankaravelu 68 | 69 | Permission is hereby granted, free of charge, to any person obtaining a copy 70 | of this software and associated documentation files (the "Software"), to deal 71 | in the Software without restriction, including without limitation the rights 72 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 73 | copies of the Software, and to permit persons to whom the Software is 74 | furnished to do so, subject to the following conditions: 75 | 76 | The above copyright notice and this permission notice shall be included in all 77 | copies or substantial portions of the Software. 78 | 79 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 80 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 81 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 82 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 83 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 84 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 85 | SOFTWARE. 86 | ``` -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """Created by Lingaraj Sankaravelu at 08:16 PM 17-10-21 using PyCharm""" 2 | import logging as log 3 | from subprocess import check_output 4 | from os import chdir 5 | from helper import CleanUpHistoryHelper 6 | from color_logger import CustomFormatter 7 | 8 | 9 | # Setting up Logger 10 | log.getLogger().setLevel(log.INFO) 11 | ch = log.StreamHandler() 12 | ch.setLevel(log.INFO) 13 | ch.setFormatter(CustomFormatter()) 14 | log.getLogger().addHandler(ch) 15 | 16 | # Important: Your local git repo PATH 17 | LOCAL_REPO_PATH = "/Users/linga/PycharmProjects/serverx/" 18 | 19 | # Prefix used for tag name example: any branch will be tagged as archive/branch_name 20 | TAG_PREFIX = "archive" 21 | 22 | # Loads the clean_up_history.json file to the helper class 23 | history = CleanUpHistoryHelper() 24 | 25 | 26 | def set_local_repo(): 27 | """Sets your project local repo""" 28 | chdir(LOCAL_REPO_PATH) 29 | log.info("Local Git repository path:%s", LOCAL_REPO_PATH) 30 | 31 | 32 | def switch_to_master(): 33 | """Switch to master and pull to get all branches Info""" 34 | run_cmd("git stash") 35 | run_cmd("git checkout master") 36 | run_cmd("git pull") 37 | log.info("Switched to master branch and pulled") 38 | 39 | 40 | def run_cmd(git_command, use_shell=True): 41 | """Run's the given git command, throws exception on failure""" 42 | return check_output(git_command, shell=use_shell) 43 | 44 | 45 | def generate_tag_name(branch_name): 46 | """Creates the Tag Name eg: archive/branch_name""" 47 | return TAG_PREFIX+branch_name if branch_name.startswith("/") else TAG_PREFIX+"/"+branch_name 48 | 49 | 50 | def create_tag(branch_name): 51 | """Create a tag for the given branch name""" 52 | tag_name = generate_tag_name(branch_name) 53 | run_cmd("git stash") 54 | run_cmd("git checkout "+branch_name) 55 | # git tag command specified as list to avoid getting "too many arguments error" 56 | tag_cmd = ["git", 57 | "tag", 58 | "-a", 59 | tag_name, 60 | "-m", 61 | "archived branch "+branch_name] 62 | run_cmd(tag_cmd, False) 63 | # pushes the created tag 64 | run_cmd("git push origin "+tag_name) 65 | log.info("Tag created: "+tag_name+"for branch:"+branch_name) 66 | return tag_name 67 | 68 | 69 | def delete_branch(branch_name): 70 | """Delete the branch matching the branch_name""" 71 | run_cmd("git checkout master") 72 | # Delete the branch 73 | run_cmd("git branch -D "+branch_name) 74 | # Push the deleted branch to remote 75 | run_cmd("git push origin :"+branch_name) 76 | log.info("Deleted branch: %s", branch_name) 77 | 78 | 79 | def delete_branches(branch_names): 80 | """Given the list of branches, delete branch one by one""" 81 | for name in branch_names: 82 | if history.is_processed(name): 83 | log.info("Branch Already Processed: %s", name) 84 | elif history.is_deleted(name): 85 | log.info("Branch Already Deleted: %s", name) 86 | else: 87 | delete_branch(name) 88 | # Marks the branch as processed and deleted in clean_up_history.json 89 | history.mark_deleted(name) 90 | 91 | 92 | def create_tags(branch_names): 93 | """Given the list of branches, create tag and delete branch one by one""" 94 | for name in branch_names: 95 | # The branch is already tagged/processed 96 | if history.is_processed(name): 97 | log.info("Branch Already Processed, Skipping:%s", name) 98 | else: 99 | tag_name = create_tag(name) 100 | delete_branch(name) 101 | history.mark_tagged(name, tag_name) 102 | 103 | 104 | if __name__ == '__main__': 105 | set_local_repo() 106 | # Add the name of the branches for the tags to be created 107 | branches = [] 108 | create_tags(branches) 109 | --------------------------------------------------------------------------------