├── .gitignore ├── README.md ├── git-batch.py └── merge.py /.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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | .static_storage/ 56 | .media/ 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # idea 107 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-batch 2 | 3 | > git-batch是一个基于`GitPython`的Git仓库批处理命令行脚本,可以批量克隆项目、更新代码、切换分支、从dev分支上创建新分支,删除本地及远端的分支等。 4 | 5 | ## 安装 6 | 1. 建议安装Python3,避免中文编码等问题。 7 | 8 | 由于Mac系统默认安装的是Python 2.7,建议下载最新的Python 3.6版本。 9 | 10 | 2. clone本项目 11 | 12 | ``` 13 | $git clone ssh://git@g.hz.netease.com:22222/hzchenwei6/git-batch.git 14 | ``` 15 | 16 | 3. 下载GitPython 17 | 18 | ``` 19 | $pip3 install gitpython 20 | ``` 21 | 22 | ## 使用 23 | * 查看帮助文档 24 | 25 | ``` 26 | $python git-batch.py -h 27 | ``` 28 | 29 | * 克隆工程 30 | 31 | ``` 32 | $python git-batch.py -f clone.txt 33 | ``` 34 | 35 | 其中clone.txt 形如 36 | 37 | ``` 38 | ssh://www.github.com/user/pro1.git 39 | ssh://www.github.com/user/pro2.git 40 | ssh://www.github.com/user/pro3.git 41 | ``` 42 | 43 | * 更新代码 44 | 45 | ``` 46 | $python git-batch.py pull 47 | ``` 48 | 49 | * 切换分支 50 | 51 | ``` 52 | $python git-batch.py checkout master 53 | $python git-batch.py co master #与上一条作用相同 54 | ``` 55 | 56 | * 从dev分支上创建新分支 57 | 58 | ``` 59 | $python git-batch.py new feature/v2.0 60 | ``` 61 | 62 | 创建新分支,也可以提供filter文件,只对filter文件中的项目创建新的分支 63 | 64 | ``` 65 | $python git-batch.py new feature/v2.0 -f filter.txt 66 | ``` 67 | 68 | filter.txt 69 | 70 | ``` 71 | pro1 72 | pro2 73 | ``` 74 | 75 | * 删除分支 76 | 77 | * 删除远端分支 78 | 79 | ``` 80 | $python git-batch.py delete feature/v1.0 -r True 81 | ``` 82 | 83 | * 删除本地分支 84 | 85 | ``` 86 | $python git-batch.py delete feature/v1.0 -r False 87 | $python git-batch.py delete feature/v1.0 # 与航一条作用相同 88 | ``` 89 | 90 | > 注意:删除本地分支时,可以不传入 -r 参数 91 | 92 | 93 | ## 简化操作 94 | 95 | 可以通过将python脚本写入`~/.bash_profile`文件,简化输入操作 96 | 97 | ``` 98 | alias python='python3' # 默认使用Python3 99 | 100 | alias gits='python /项目目录/git-batch/git-batch.py' 101 | ``` 102 | 103 | 后续只要使用`gits`命令就可以执行该脚本。 104 | 105 | ``` 106 | $gits clone -f clone.txt -p ~/Users/netease/study-project # 克隆项目 107 | $gits pull -p ~/Users/netease/study-project # 更新代码 108 | $gits checkout master -p ~/Users/netease/study-project # 切换分支 109 | $gits new release/v2.0 -p ~/Users/netease/study-project # 从dev上拉取新分支 110 | $gits delete feature/v1.0 -p ~/Users/netease/study-project # 删除本地分支 111 | $gits delete feature/v1.0 -r true -p ~/Users/netease/study-project # 删除远端分支 112 | ``` 113 | 114 | 为了避免每次输入`-p 项目根目录`,可以先切换到项目根目录 115 | 116 | ``` 117 | $cd ~/Users/netease/study-project 118 | $gits clone -f clone.txt # 克隆项目 119 | $gits pull # 更新代码 120 | $gits checkout master # 切换分支 121 | $gits new release/v2.0 # 从dev上创建新分支 122 | $gits new release/v2.0 -f filter.txt # 创建新分支时指定项目列表 123 | $gits delete feature/v1.0 # 删除本地分支 124 | $gits delete feature/v1.0 -r true # 删除远端分支 125 | ``` -------------------------------------------------------------------------------- /git-batch.py: -------------------------------------------------------------------------------- 1 | from git import Repo, InvalidGitRepositoryError, GitCommandError, Git 2 | from os import listdir 3 | from os.path import join 4 | import argparse 5 | 6 | # 暂存列表 7 | stash_repos = [] 8 | 9 | 10 | def path_to_repo(path): 11 | """将路径转换成Repo对象,若非Git目录,则抛出异常""" 12 | try: 13 | return Repo(path) 14 | except InvalidGitRepositoryError: 15 | return None 16 | 17 | 18 | def not_none(obj): 19 | return obj is not None 20 | 21 | 22 | def get_all_git_repos(path): 23 | """获取指定路径中的全部Git目录""" 24 | return list(filter(not_none, map(path_to_repo, map(join, [path] * len(listdir(path)), listdir(path))))) 25 | 26 | 27 | def pull_repos(repos): 28 | """拉取到最新代码""" 29 | for repo in repos: 30 | git_pull_single_repo(repo) 31 | 32 | 33 | def git_pull_single_repo(repo): 34 | """拉取到最新代码""" 35 | stashed = False 36 | if repo.is_dirty(): 37 | print(repo.git_dir + " 包含未提交文件,已暂存。") 38 | repo.git.stash('save') 39 | stash_repos.append(repo) 40 | stashed = True 41 | repo.remote().pull() 42 | print(repo.working_dir.split('/')[-1] + ' pull finished.') 43 | if stashed: 44 | try: 45 | repo.git.stash('pop') 46 | print(repo.git_dir + " stash pop finished.") 47 | except GitCommandError: 48 | print(repo.git_dir + " merge conflict, please merge by yourself.") 49 | 50 | 51 | def get_branch_name(branch): 52 | return branch.name 53 | 54 | 55 | def get_remote_branch_name(branch_name): 56 | return 'origin/' + branch_name 57 | 58 | 59 | def checkout_repos(repos, branch): 60 | """切换分支""" 61 | for repo in repos: 62 | checkout(repo, branch) 63 | 64 | 65 | def get_all_local_branches(repo): 66 | """获取本地分支""" 67 | return list(map(get_branch_name, repo.branches)) 68 | 69 | 70 | def get_all_remote_branches(repo): 71 | """获取远端分支""" 72 | return list(map(get_branch_name, repo.remotes.origin.refs)) 73 | 74 | 75 | def checkout(repo, branch, log=True): 76 | # 远端分支名称 77 | remote_branch = get_remote_branch_name(branch) 78 | try: 79 | if branch in get_all_local_branches(repo): 80 | # 如果存在本地分支,则直接checkout到本地分支 81 | repo.git.checkout(branch) 82 | if log: 83 | print(get_repo_dir_name(repo) + ' checkout finished.') 84 | elif remote_branch in get_all_remote_branches(repo): 85 | # 如果存在远端分支,则追踪至远端分支 86 | repo.git.checkout(remote_branch, b=branch) 87 | if log: 88 | print(get_repo_dir_name(repo) + ' checkout finished.') 89 | else: 90 | if log: 91 | print(get_repo_dir_name(repo) + ' does not have this branch.') 92 | except GitCommandError: 93 | print("TODO") 94 | 95 | 96 | def create_branches(repos, branch, filter_file): 97 | """拉取新分支""" 98 | if filter_file: 99 | # 传入过滤文件,则仅从过滤文件中拉取新分支 100 | handle_dirs = [] 101 | with open(filter_file, 'r') as f: 102 | for handle_dir in f: 103 | handle_dirs.append(handle_dir.replace('\n', '')) 104 | for repo in repos: 105 | if get_repo_dir_name(repo) not in handle_dirs: 106 | return 107 | create_branch(repo, branch) 108 | else: 109 | for repo in repos: 110 | create_branch(repo, branch) 111 | 112 | 113 | def create_branch(repo, branch): 114 | # 切换至dev分支 115 | checkout(repo, 'dev', log=False) 116 | # 创建本地分支 117 | repo.create_head(branch) 118 | # 切换至新分支 119 | checkout(repo, branch, log=False) 120 | # push到远端 121 | repo.git.push('origin', branch) 122 | print(get_repo_dir_name(repo) + ' create new branch and push to origin.') 123 | 124 | 125 | def get_repo_dir_name(repo): 126 | """返回仓库文件夹名称""" 127 | return repo.working_dir.split('/')[-1] 128 | 129 | 130 | def delete_branches(repos, branch, remote=False): 131 | """删除分支""" 132 | for repo in repos: 133 | delete_branch(repo, branch, remote) 134 | 135 | 136 | def delete_branch(repo, branch, remote=False): 137 | """删除分支""" 138 | if remote: 139 | delete_remote_branch(branch, repo) 140 | else: 141 | delete_local_branch(branch, repo) 142 | 143 | 144 | def delete_local_branch(branch, repo): 145 | """删除本地分支""" 146 | if repo.active_branch.name == branch: 147 | print(get_repo_dir_name(repo)) 148 | print('Cannot delete the branch which you are currently on.') 149 | print() 150 | elif branch not in get_all_local_branches(repo): 151 | print(get_repo_dir_name(repo) + ' branch not found.') 152 | print() 153 | else: 154 | repo.delete_head(branch) 155 | print(get_repo_dir_name(repo) + ' delete ' + branch + ' finished.') 156 | print() 157 | 158 | 159 | def delete_remote_branch(branch, repo): 160 | """删除远端分支""" 161 | remote_branch = get_remote_branch_name(branch) 162 | if remote_branch not in get_all_remote_branches(repo): 163 | print(get_repo_dir_name(repo) + ' branch not found.') 164 | print() 165 | else: 166 | remote = repo.remote(name='origin') 167 | remote.push(refspec=(':' + branch)) 168 | print(get_repo_dir_name(repo) + ' delete ' + branch + ' finished.') 169 | print() 170 | 171 | 172 | def clone_repos(path, clone_file): 173 | with open(clone_file, 'r') as f: 174 | for repo_url in f: 175 | clone_repo(path, repo_url.replace('\n', '')) 176 | 177 | 178 | def clone_repo(path, repo_url): 179 | """克隆仓库""" 180 | try: 181 | Git(path).clone(repo_url) 182 | print('Clone ' + repo_url + ' finished.') 183 | except GitCommandError: 184 | print('Clone ' + repo_url + ' failed.') 185 | 186 | 187 | def handle_args(): 188 | """解析脚本参数""" 189 | method = args.method 190 | if method == 'clone': 191 | if args.filter: 192 | clone_repos(args.path, args.filter) 193 | return 194 | else: 195 | print("克隆工程需要filter文件,指定克隆项目列表") 196 | return 197 | 198 | repos = get_all_git_repos(args.path) # 获取全部仓库 199 | if method == 'pull': 200 | """拉取最新代码""" 201 | pull_repos(repos) 202 | elif (method == 'checkout' or method == 'co') and args.branch != '': 203 | """切换到指定分支""" 204 | checkout_repos(repos, args.branch) 205 | elif method == 'new' and args.branch != '': 206 | """创建新分支""" 207 | create_branches(repos, args.branch, args.filter) 208 | elif method == 'delete' and args.branch != '': 209 | """删除分支""" 210 | delete_branches(repos, args.branch, args.remote) 211 | else: 212 | print("Not support method") 213 | 214 | 215 | parser = argparse.ArgumentParser(description='Git 批处理工具') 216 | parser.add_argument('-p', '--path', type=str, default='.', help='批处理目录,默认为当前目录', required=False) 217 | parser.add_argument('-r', '--remote', type=bool, default=False, help='是否操作远端分支,默认为False', required=False) 218 | parser.add_argument('-f', '--filter', type=str, help='克隆项目目标文件', required=False) 219 | parser.add_argument('method', action='store', type=str, choices=['clone', 'pull', 'checkout', 'co', 'new', 'delete'], 220 | help='批量执行任务, clone, pull, checkout[co], new, delete') 221 | parser.add_argument('branch', nargs='?', action='store', type=str, default='', help='指定target分支') 222 | 223 | args = parser.parse_args() 224 | 225 | handle_args() 226 | -------------------------------------------------------------------------------- /merge.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/git-batch/2bb1113a869e32204ca47db9af74c4aac55c5157/merge.py --------------------------------------------------------------------------------