pygwalk 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygwalk-0.2.1/LICENSE +21 -0
- pygwalk-0.2.1/PKG-INFO +118 -0
- pygwalk-0.2.1/README.md +91 -0
- pygwalk-0.2.1/gwalk/__init__.py +0 -0
- pygwalk-0.2.1/gwalk/gapply.py +83 -0
- pygwalk-0.2.1/gwalk/gcp.py +91 -0
- pygwalk-0.2.1/gwalk/gl.py +40 -0
- pygwalk-0.2.1/gwalk/gwalk.py +547 -0
- pygwalk-0.2.1/pygwalk.egg-info/PKG-INFO +118 -0
- pygwalk-0.2.1/pygwalk.egg-info/SOURCES.txt +15 -0
- pygwalk-0.2.1/pygwalk.egg-info/dependency_links.txt +1 -0
- pygwalk-0.2.1/pygwalk.egg-info/entry_points.txt +5 -0
- pygwalk-0.2.1/pygwalk.egg-info/requires.txt +2 -0
- pygwalk-0.2.1/pygwalk.egg-info/top_level.txt +1 -0
- pygwalk-0.2.1/pyproject.toml +49 -0
- pygwalk-0.2.1/setup.cfg +4 -0
- pygwalk-0.2.1/tests/test_gwalk.py +42 -0
pygwalk-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020-2024 Zero <zero.kwok@foxmail.com>
|
|
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.
|
pygwalk-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pygwalk
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: gwalk 是一系列用于管理 Git 仓库的命令行小工具,帮助开发者对大批量的 Git 仓库进行日常维护。
|
|
5
|
+
Author-email: Zero Kwok <zero.kwok@foxmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Project-URL: Homepage, https://github.com/ZeroKwok/gwalk
|
|
8
|
+
Project-URL: Issues, https://github.com/ZeroKwok/gwalk/issues
|
|
9
|
+
Keywords: git,tools
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Requires-Python: >=3.7
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: GitPython
|
|
25
|
+
Requires-Dist: termcolor
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# gwalk
|
|
29
|
+
|
|
30
|
+
`gwalk` 是一系列用于管理 Git 仓库的命令行小工具,帮助开发者对大批量的 Git 仓库进行日常维护。
|
|
31
|
+
|
|
32
|
+
## 安装
|
|
33
|
+
|
|
34
|
+
### 1. pip
|
|
35
|
+
|
|
36
|
+
1. `python -m pip install pygwalk`
|
|
37
|
+
|
|
38
|
+
### 2. build from source
|
|
39
|
+
|
|
40
|
+
1. `git clone https://github.com/ZeroKwok/gwalk.git`
|
|
41
|
+
2. `cd gwalk`
|
|
42
|
+
3. `python -m pip install .`
|
|
43
|
+
|
|
44
|
+
## 使用
|
|
45
|
+
|
|
46
|
+
### 1. gl
|
|
47
|
+
|
|
48
|
+
`gl.py` 是 `git pull` 操作的快捷工具。
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 从远程仓库拉取代码并合并到当前分支, 等价于下面的命令
|
|
52
|
+
# git pull {origin 或 第一个remotes} {当前分支}
|
|
53
|
+
gl
|
|
54
|
+
|
|
55
|
+
# git pull {origin 或 第一个remotes} {当前分支} --rebase
|
|
56
|
+
gl --rebase
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. gcp
|
|
60
|
+
|
|
61
|
+
`gcp.py` 是用于执行 `git commit` 和 `git push` 操作快捷工具。
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# 添加未跟踪的文件以及已修改的文件,并提交到远程仓库, 等价于下面的命令
|
|
65
|
+
# git add -u && git commit -m "fix some bugs" && git push
|
|
66
|
+
gcp "fix some bugs"
|
|
67
|
+
|
|
68
|
+
# 仅推送当前分支到所有远程仓库,不进行提交
|
|
69
|
+
gcp -p
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3. gwalk
|
|
73
|
+
|
|
74
|
+
`gwalk.py` 是 `gwalk` 工具的主要组件,提供了以下功能:
|
|
75
|
+
|
|
76
|
+
- 列出目录下的所有 Git 仓库,支持过滤条件、黑名单、白名单和目录递归。
|
|
77
|
+
- 显示列出的仓库的状态信息,支持输出信息的简短或冗长格式。
|
|
78
|
+
- 在每个列出的仓库中执行一个操作。如运行自定义命令: 类似于子仓库操作 `git submodule foreach 'some command'` 但更加灵活。
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# 列出当前目录下所有的'脏'的 Git 仓库
|
|
82
|
+
gwalk
|
|
83
|
+
|
|
84
|
+
# 递归列出当前目录下所有的 Git 仓库
|
|
85
|
+
gwalk -rf all
|
|
86
|
+
|
|
87
|
+
# 在列出的每个仓库中执行命令: git pull origin
|
|
88
|
+
gwalk -rf all -a "run git pull origin"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 使用技巧
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# 在所有 gwalk 列出的仓库中, 执行 gl 工具(git pull)
|
|
95
|
+
gwalk -rf all -a run gl
|
|
96
|
+
|
|
97
|
+
# 在所有 gwalk 列出的仓库中, 执行 git push 操作 {ab} 表示 当前分支(ActiveBranch)
|
|
98
|
+
gwalk -rf all -a run git push second {ab}
|
|
99
|
+
|
|
100
|
+
# 批量手动处理(交互模式)
|
|
101
|
+
# 在列出的所有 '包含未提交的修改' 的仓库中, 启动一个 bash shell 来接受用户的操作
|
|
102
|
+
gwalk -rf modified --a bash
|
|
103
|
+
|
|
104
|
+
# 批量推送
|
|
105
|
+
# 在列出的所有 '包含未提交的修改 且 不再黑名单中' 的仓库中, 运行 gcp 工具, 推送当前分支到所有远程仓库
|
|
106
|
+
gwalk -rf modified --blacklist gwalk.blacklist --a "gcp -p"
|
|
107
|
+
|
|
108
|
+
# 批量打标签
|
|
109
|
+
# 在列出的所有 白名单 gwalk.whitelist 匹配的仓库中, 运行 git tag v1.5.0
|
|
110
|
+
gwalk -rf all --whitelist gwalk.whitelist -a run git tag v1.5.0
|
|
111
|
+
|
|
112
|
+
# 批量查看目录下所有仓库的最近3次提交
|
|
113
|
+
gwalk -f all -l none -a run "git log --oneline -n3"
|
|
114
|
+
|
|
115
|
+
# 批量替换 origin 远程仓库的地址, 从 github.com 替换成 gitee.com
|
|
116
|
+
# 在所有 gwalk 列出的仓库中, 执行自定义命令
|
|
117
|
+
gwalk -rf all -a run git remote set-url origin `echo \`git remote get-url origin\` | python -c "print(input().replace('github.com', 'gitee.com'))"`
|
|
118
|
+
```
|
pygwalk-0.2.1/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# gwalk
|
|
2
|
+
|
|
3
|
+
`gwalk` 是一系列用于管理 Git 仓库的命令行小工具,帮助开发者对大批量的 Git 仓库进行日常维护。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
### 1. pip
|
|
8
|
+
|
|
9
|
+
1. `python -m pip install pygwalk`
|
|
10
|
+
|
|
11
|
+
### 2. build from source
|
|
12
|
+
|
|
13
|
+
1. `git clone https://github.com/ZeroKwok/gwalk.git`
|
|
14
|
+
2. `cd gwalk`
|
|
15
|
+
3. `python -m pip install .`
|
|
16
|
+
|
|
17
|
+
## 使用
|
|
18
|
+
|
|
19
|
+
### 1. gl
|
|
20
|
+
|
|
21
|
+
`gl.py` 是 `git pull` 操作的快捷工具。
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 从远程仓库拉取代码并合并到当前分支, 等价于下面的命令
|
|
25
|
+
# git pull {origin 或 第一个remotes} {当前分支}
|
|
26
|
+
gl
|
|
27
|
+
|
|
28
|
+
# git pull {origin 或 第一个remotes} {当前分支} --rebase
|
|
29
|
+
gl --rebase
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. gcp
|
|
33
|
+
|
|
34
|
+
`gcp.py` 是用于执行 `git commit` 和 `git push` 操作快捷工具。
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# 添加未跟踪的文件以及已修改的文件,并提交到远程仓库, 等价于下面的命令
|
|
38
|
+
# git add -u && git commit -m "fix some bugs" && git push
|
|
39
|
+
gcp "fix some bugs"
|
|
40
|
+
|
|
41
|
+
# 仅推送当前分支到所有远程仓库,不进行提交
|
|
42
|
+
gcp -p
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 3. gwalk
|
|
46
|
+
|
|
47
|
+
`gwalk.py` 是 `gwalk` 工具的主要组件,提供了以下功能:
|
|
48
|
+
|
|
49
|
+
- 列出目录下的所有 Git 仓库,支持过滤条件、黑名单、白名单和目录递归。
|
|
50
|
+
- 显示列出的仓库的状态信息,支持输出信息的简短或冗长格式。
|
|
51
|
+
- 在每个列出的仓库中执行一个操作。如运行自定义命令: 类似于子仓库操作 `git submodule foreach 'some command'` 但更加灵活。
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# 列出当前目录下所有的'脏'的 Git 仓库
|
|
55
|
+
gwalk
|
|
56
|
+
|
|
57
|
+
# 递归列出当前目录下所有的 Git 仓库
|
|
58
|
+
gwalk -rf all
|
|
59
|
+
|
|
60
|
+
# 在列出的每个仓库中执行命令: git pull origin
|
|
61
|
+
gwalk -rf all -a "run git pull origin"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 使用技巧
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 在所有 gwalk 列出的仓库中, 执行 gl 工具(git pull)
|
|
68
|
+
gwalk -rf all -a run gl
|
|
69
|
+
|
|
70
|
+
# 在所有 gwalk 列出的仓库中, 执行 git push 操作 {ab} 表示 当前分支(ActiveBranch)
|
|
71
|
+
gwalk -rf all -a run git push second {ab}
|
|
72
|
+
|
|
73
|
+
# 批量手动处理(交互模式)
|
|
74
|
+
# 在列出的所有 '包含未提交的修改' 的仓库中, 启动一个 bash shell 来接受用户的操作
|
|
75
|
+
gwalk -rf modified --a bash
|
|
76
|
+
|
|
77
|
+
# 批量推送
|
|
78
|
+
# 在列出的所有 '包含未提交的修改 且 不再黑名单中' 的仓库中, 运行 gcp 工具, 推送当前分支到所有远程仓库
|
|
79
|
+
gwalk -rf modified --blacklist gwalk.blacklist --a "gcp -p"
|
|
80
|
+
|
|
81
|
+
# 批量打标签
|
|
82
|
+
# 在列出的所有 白名单 gwalk.whitelist 匹配的仓库中, 运行 git tag v1.5.0
|
|
83
|
+
gwalk -rf all --whitelist gwalk.whitelist -a run git tag v1.5.0
|
|
84
|
+
|
|
85
|
+
# 批量查看目录下所有仓库的最近3次提交
|
|
86
|
+
gwalk -f all -l none -a run "git log --oneline -n3"
|
|
87
|
+
|
|
88
|
+
# 批量替换 origin 远程仓库的地址, 从 github.com 替换成 gitee.com
|
|
89
|
+
# 在所有 gwalk 列出的仓库中, 执行自定义命令
|
|
90
|
+
gwalk -rf all -a run git remote set-url origin `echo \`git remote get-url origin\` | python -c "print(input().replace('github.com', 'gitee.com'))"`
|
|
91
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#! python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# This file is part of the gwalk project.
|
|
5
|
+
# Copyright (c) 2020-2024 zero <zero.kwok@foxmail.com>
|
|
6
|
+
#
|
|
7
|
+
# For the full copyright and license information, please view the LICENSE
|
|
8
|
+
# file that was distributed with this source code.
|
|
9
|
+
#
|
|
10
|
+
# gapply.py (git apply patch and create commit)
|
|
11
|
+
#
|
|
12
|
+
# Syntax:
|
|
13
|
+
# gapply.py [-h] <patch_file ...>
|
|
14
|
+
#
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
from gwalk import gwalk
|
|
20
|
+
|
|
21
|
+
def extract_subject_from_patch(patch_file):
|
|
22
|
+
with open(patch_file, 'r') as file:
|
|
23
|
+
lines = file.readlines()
|
|
24
|
+
for line in lines:
|
|
25
|
+
if line.startswith("Subject:"):
|
|
26
|
+
subject = line[len("Subject:"):].strip()
|
|
27
|
+
# Remove [PATCH X/Y] if it exists
|
|
28
|
+
subject = re.sub(r'\[PATCH [0-9]+/[0-9]+\] ', '', subject)
|
|
29
|
+
return subject
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
def extract_subject_from_filename(patch_file):
|
|
33
|
+
filename = os.path.splitext(os.path.basename(patch_file))[0]
|
|
34
|
+
subject = re.sub(r'^[0-9]+-', '', filename)
|
|
35
|
+
subject = re.sub(r'\.patch$', '', subject)
|
|
36
|
+
subject = subject.replace('-', ' ')
|
|
37
|
+
return subject
|
|
38
|
+
|
|
39
|
+
def apply_patch(patch_file):
|
|
40
|
+
cmd = f'git apply -v "{patch_file}"'
|
|
41
|
+
gwalk.cprint(f'> {cmd}', 'green')
|
|
42
|
+
result = gwalk.RepoHandler.execute(cmd)
|
|
43
|
+
if result != 0:
|
|
44
|
+
gwalk.cprint(f"Failed to apply patch: {patch_file}", 'red')
|
|
45
|
+
sys.exit(result)
|
|
46
|
+
|
|
47
|
+
def stage_changes():
|
|
48
|
+
cmd = f'git add -u'
|
|
49
|
+
gwalk.cprint(f'> {cmd}', 'green')
|
|
50
|
+
result = gwalk.RepoHandler.execute(cmd)
|
|
51
|
+
if result != 0:
|
|
52
|
+
gwalk.cprint(f"Failed to stage changes.", 'red')
|
|
53
|
+
sys.exit(result)
|
|
54
|
+
|
|
55
|
+
def commit_changes(subject):
|
|
56
|
+
cmd = f'git commit -m "{subject}"'
|
|
57
|
+
gwalk.cprint(f'> {cmd}', 'green')
|
|
58
|
+
result = gwalk.RepoHandler.execute(cmd)
|
|
59
|
+
if result != 0:
|
|
60
|
+
gwalk.cprint(f"Failed to create commit.", 'red')
|
|
61
|
+
sys.exit(result)
|
|
62
|
+
|
|
63
|
+
def main():
|
|
64
|
+
if len(sys.argv) < 2:
|
|
65
|
+
print("Usage: gapply.py <patch_file ...>")
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
for patch_file in sys.argv[1:]:
|
|
69
|
+
if not os.path.isfile(patch_file):
|
|
70
|
+
gwalk.cprint(f"Patch file {patch_file} does not exist", 'red')
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
subject = extract_subject_from_patch(patch_file)
|
|
74
|
+
if not subject:
|
|
75
|
+
subject = extract_subject_from_filename(patch_file)
|
|
76
|
+
|
|
77
|
+
apply_patch(patch_file)
|
|
78
|
+
stage_changes()
|
|
79
|
+
commit_changes(subject)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
main()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#! python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# This file is part of the gwalk project.
|
|
5
|
+
# Copyright (c) 2020-2024 zero <zero.kwok@foxmail.com>
|
|
6
|
+
#
|
|
7
|
+
# For the full copyright and license information, please view the LICENSE
|
|
8
|
+
# file that was distributed with this source code.
|
|
9
|
+
#
|
|
10
|
+
# gcp.py (git commit and push)
|
|
11
|
+
#
|
|
12
|
+
# 语法
|
|
13
|
+
# gcp.py [-h] [-a|--all] [--show] [-p|--push] [-s|--src <SRC>] ["提交信息"]
|
|
14
|
+
#
|
|
15
|
+
# 示例
|
|
16
|
+
# 1. gcp.py "fix some bugs"
|
|
17
|
+
# 仅推送当前分支到所有远端, 不做提交
|
|
18
|
+
# 相当于执行: git add -u && git commit -m "提交信息" && git push {remotes} {branch}
|
|
19
|
+
# 2. gcp.py --push
|
|
20
|
+
# 仅推送当前分支到所有远端, 不做提交
|
|
21
|
+
#
|
|
22
|
+
# 选项
|
|
23
|
+
# --show 仅显示执行命令,而不做任何改变
|
|
24
|
+
# -a,--all 添加未跟踪的文件以及已修改的文件
|
|
25
|
+
# -s,--src 要推送的本地仓库中的 分支 或 标签
|
|
26
|
+
# -p,--push 仅执行推送动作, 将忽略 --all 以及 commit
|
|
27
|
+
# commit 提交消息
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import argparse
|
|
31
|
+
from gwalk import gwalk
|
|
32
|
+
|
|
33
|
+
class ResultError(RuntimeError):
|
|
34
|
+
def __init__(self, message, ecode):
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
self.ecode = ecode
|
|
37
|
+
|
|
38
|
+
def execute(commands:str, onlyShow:bool=False):
|
|
39
|
+
gwalk.cprint(commands, 'green')
|
|
40
|
+
if onlyShow:
|
|
41
|
+
return
|
|
42
|
+
code = gwalk.RepoHandler.execute(commands)
|
|
43
|
+
if code != 0:
|
|
44
|
+
raise ResultError(f'Run: {commands}', code)
|
|
45
|
+
|
|
46
|
+
def main():
|
|
47
|
+
try:
|
|
48
|
+
parser = argparse.ArgumentParser()
|
|
49
|
+
parser.add_argument('-a', '--all', action='store_true')
|
|
50
|
+
parser.add_argument('-s', '--src', action='store', default=None)
|
|
51
|
+
parser.add_argument('-p', '--push', action='store_true')
|
|
52
|
+
parser.add_argument('--show', action='store_true')
|
|
53
|
+
parser.add_argument('commit', nargs=argparse.REMAINDER)
|
|
54
|
+
args = parser.parse_args()
|
|
55
|
+
|
|
56
|
+
args.commit = ' '.join(args.commit)
|
|
57
|
+
|
|
58
|
+
if not gwalk.RepoWalk.isRepo(os.getcwd()):
|
|
59
|
+
gwalk.cprint(f'This is not an valid git repository.', 'red')
|
|
60
|
+
exit(1)
|
|
61
|
+
|
|
62
|
+
repo = gwalk.RepoStatus(os.getcwd()).load()
|
|
63
|
+
|
|
64
|
+
if args.src is None:
|
|
65
|
+
args.src = repo.repo.active_branch.name
|
|
66
|
+
|
|
67
|
+
if args.push:
|
|
68
|
+
for r in repo.repo.remotes:
|
|
69
|
+
execute(f'git push {r.name} {args.src}', args.show)
|
|
70
|
+
exit(0)
|
|
71
|
+
|
|
72
|
+
if repo.match('clean'):
|
|
73
|
+
gwalk.cprint(f'The git repository is clean.', 'green')
|
|
74
|
+
exit(0)
|
|
75
|
+
execute('git status -s --untracked-files=normal')
|
|
76
|
+
|
|
77
|
+
if repo.match('dirty' if args.all else 'modified'):
|
|
78
|
+
execute('git add -A' if args.all else 'git add -u', args.show)
|
|
79
|
+
if args.commit:
|
|
80
|
+
execute(f'git commit -m "{args.commit}"', args.show)
|
|
81
|
+
else:
|
|
82
|
+
execute(f'git commit', args.show)
|
|
83
|
+
for r in repo.repo.remotes:
|
|
84
|
+
execute(f'git push {r.name} {args.src}', args.show)
|
|
85
|
+
exit(0)
|
|
86
|
+
except ResultError as e:
|
|
87
|
+
exit(e.ecode)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == '__main__':
|
|
91
|
+
main()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#! python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# This file is part of the gwalk project.
|
|
5
|
+
# Copyright (c) 2020-2024 zero <zero.kwok@foxmail.com>
|
|
6
|
+
#
|
|
7
|
+
# For the full copyright and license information, please view the LICENSE
|
|
8
|
+
# file that was distributed with this source code.
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import argparse
|
|
12
|
+
from gwalk import gwalk
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
parser = argparse.ArgumentParser()
|
|
16
|
+
parser.add_argument('--rebase', action='store_true')
|
|
17
|
+
args = parser.parse_args()
|
|
18
|
+
|
|
19
|
+
if not gwalk.RepoWalk.isRepo(os.getcwd()):
|
|
20
|
+
gwalk.cprint(f'This is not an valid git repository.', 'red')
|
|
21
|
+
exit(1)
|
|
22
|
+
|
|
23
|
+
repo = gwalk.git.Repo(os.getcwd())
|
|
24
|
+
branch = repo.active_branch.name
|
|
25
|
+
remote = 'origin'
|
|
26
|
+
if not remote in repo.remotes:
|
|
27
|
+
if len(repo.remotes) > 0:
|
|
28
|
+
remote = repo.remotes[0].name
|
|
29
|
+
|
|
30
|
+
rebase = ''
|
|
31
|
+
if args.rebase:
|
|
32
|
+
rebase = '--rebase'
|
|
33
|
+
|
|
34
|
+
cmd = f'git pull {remote} {branch} {rebase}'
|
|
35
|
+
gwalk.cprint(f'> {cmd}', 'green')
|
|
36
|
+
exit(gwalk.RepoHandler.execute(cmd))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == '__main__':
|
|
40
|
+
main()
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
#! python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# This file is part of the gwalk project.
|
|
5
|
+
# Copyright (c) 2020-2024 zero <zero.kwok@foxmail.com>
|
|
6
|
+
#
|
|
7
|
+
# For the full copyright and license information, please view the LICENSE
|
|
8
|
+
# file that was distributed with this source code.
|
|
9
|
+
#
|
|
10
|
+
# gwalk.py
|
|
11
|
+
#
|
|
12
|
+
# 功能性需求
|
|
13
|
+
# - 列出指定目录下的所有的Git仓库
|
|
14
|
+
# - 可指定过滤条件
|
|
15
|
+
# - 存在未提交的修改
|
|
16
|
+
# - 存在未跟踪的文件
|
|
17
|
+
# - 脏的: 存在未提交的修改 或 未跟踪的文件 (默认)
|
|
18
|
+
# - 所有
|
|
19
|
+
# - 可指定黑名单, 即跳过匹配的仓库
|
|
20
|
+
# - 可指定白名单, 即列出匹配的仓库
|
|
21
|
+
# - 可指定是否递归搜索目录
|
|
22
|
+
# - 显示列出仓库的状态信息
|
|
23
|
+
# - 可指定输出信息是简短还是冗长
|
|
24
|
+
# - 指定在每个列出的仓库中执行的任务
|
|
25
|
+
# - run CMD
|
|
26
|
+
# 在仓库中执行命令: CMD
|
|
27
|
+
# - git gui
|
|
28
|
+
# 在仓库中唤起: Git Gui
|
|
29
|
+
# - git bash
|
|
30
|
+
# 在仓库中唤起: Git Bash
|
|
31
|
+
#
|
|
32
|
+
# 语法
|
|
33
|
+
# gwalk.py [--help][--version] [--verbose | --level LEVEL]
|
|
34
|
+
# [--directory PATH] [--recursive] [--blacklist FILENAME] [--force]
|
|
35
|
+
# [--filter CONDITION] [--action PROC]
|
|
36
|
+
# 示例
|
|
37
|
+
# 1. gwalk.py
|
|
38
|
+
# 列出当前目录下所有'脏'的Git仓库(不包含./gwalk.blacklist黑名单文件中匹配的项)
|
|
39
|
+
# 2. gwalk.py -f all -r
|
|
40
|
+
# gwalk.py -rf all
|
|
41
|
+
# 递归列出当前目录下所有的Git仓库(不包含./gwalk.blacklist黑名单文件中匹配的项)
|
|
42
|
+
# 3. gwalk.py -rf all -a run git pull origin
|
|
43
|
+
# 在列出的每个仓库中执行命令: git pull origin
|
|
44
|
+
#
|
|
45
|
+
# 选项
|
|
46
|
+
# -h,--help 显示帮助信息
|
|
47
|
+
# --version 输出程序版本
|
|
48
|
+
# --debug 启用调试输出, 辅助调试过滤条件与执行的命令
|
|
49
|
+
#
|
|
50
|
+
# -v,--verbose 列出仓库的同时, 还会输出尽可能详细的状态信息
|
|
51
|
+
# -l,--level 指定输出等级
|
|
52
|
+
# 'none' 仅列出仓库, 不打印仓库状态
|
|
53
|
+
# 'brief' 列出仓库的同时, 还会输出只有一行的简短信息(默认)
|
|
54
|
+
# 'normal' 列出仓库的同时, 还会输出一般的仓库的状态信息
|
|
55
|
+
# 'verbose' 同--verbose
|
|
56
|
+
#
|
|
57
|
+
# -d,--directory 指定目录下搜索Git仓库, 默认是程序的当前目录
|
|
58
|
+
# -r,--recursive 指定是否进入子文件夹, 从而列出尽可能多的仓库
|
|
59
|
+
# -f,--filter 指定搜索仓库的过滤条件, 只有满足条件的仓库将被列出
|
|
60
|
+
# 'all' 所有仓库
|
|
61
|
+
# 'clean' 所有'干净'的仓库
|
|
62
|
+
# 'dirty' 所有'脏'的仓库(默认)
|
|
63
|
+
# 'modified' 所有'包含未提交的修改'的仓库
|
|
64
|
+
# 'untracked' 所有'包含跟踪文件的修改'的仓库
|
|
65
|
+
# --blacklist 指定黑名单文件, 若黑名单文件有效, 则将忽略所有匹配的仓库, 即使指定了
|
|
66
|
+
# --condition=all.
|
|
67
|
+
# 黑名单采用正则表达式匹配仓库目录的绝对路径 (分隔符统一为: '/' Unix filesystem分隔符)
|
|
68
|
+
# 示例如下:
|
|
69
|
+
# ^.+/3rd$
|
|
70
|
+
# ^.+/demo$
|
|
71
|
+
# 若--blacklist未指定, 则尝试在--directory指定的目录下查找 gwalk.blacklist文件.
|
|
72
|
+
#
|
|
73
|
+
# --whitelist 指定白名单文件, 使黑名单机制失效. 若白名单文件有效, 则将忽略所有未匹配的仓库.
|
|
74
|
+
# --force 使黑名单机制失效, 即使指定了--blacklist
|
|
75
|
+
#
|
|
76
|
+
# -a,--action 指定在每个列出的仓库中执行的任务
|
|
77
|
+
# 'gui' 在每个列出的仓库唤出 Git Gui 来处理用户的操作
|
|
78
|
+
# 'bash' 在每个列出的仓库唤出 Git Bash 来处理用户的操作
|
|
79
|
+
# 'run CMD' 在每个列出的仓库执行 CMD 指定的命令, 可以在命令中包含下列"变量"(不区分大小写),
|
|
80
|
+
# 这将在执行前被替换为相应的内容.
|
|
81
|
+
# {ab} 或 {ActiveBranch} 将被替换为当前仓库的 活动分支名
|
|
82
|
+
# {RepositoryName} 将被替换为当前仓库的 目录名
|
|
83
|
+
# {cwd} 将被替换为 --directory 指定的目录, 因为 . 是当前仓库的目录.
|
|
84
|
+
|
|
85
|
+
import os
|
|
86
|
+
import re
|
|
87
|
+
import sys
|
|
88
|
+
import shutil
|
|
89
|
+
import argparse
|
|
90
|
+
import platform
|
|
91
|
+
import traceback
|
|
92
|
+
|
|
93
|
+
projectName = 'gwalk'
|
|
94
|
+
projectHome = 'https://github.com/ZeroKwok/gwalk.git'
|
|
95
|
+
projectVersion = '0.2.1'
|
|
96
|
+
projectAuthor = 'zero.kwok@foxmail.com'
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
import git
|
|
100
|
+
from termcolor import cprint
|
|
101
|
+
except ModuleNotFoundError as e:
|
|
102
|
+
print(f'{projectName} depends on "GitPython" and "termcolor", use the following command to install them:')
|
|
103
|
+
print()
|
|
104
|
+
print('"python -m pip install GitPython termcolor"')
|
|
105
|
+
exit(1)
|
|
106
|
+
except KeyboardInterrupt:
|
|
107
|
+
exit(1)
|
|
108
|
+
|
|
109
|
+
class RepoWalk:
|
|
110
|
+
def __init__(self, directory:str, recursive:bool=False, debug:bool=False):
|
|
111
|
+
self.directory = directory
|
|
112
|
+
self.recursive = recursive
|
|
113
|
+
self.debug = debug
|
|
114
|
+
|
|
115
|
+
def __iter__(self):
|
|
116
|
+
if self.recursive:
|
|
117
|
+
for root, dirs, files in os.walk(self.directory):
|
|
118
|
+
if RepoWalk.repoType(dirs, files) != 0:
|
|
119
|
+
yield root
|
|
120
|
+
else:
|
|
121
|
+
for root, dirs, files in os.walk(self.directory):
|
|
122
|
+
if RepoWalk.isRepo(root):
|
|
123
|
+
yield root
|
|
124
|
+
for d in dirs:
|
|
125
|
+
if d in ['.git', '.vs', '.vscode']:
|
|
126
|
+
continue
|
|
127
|
+
path = os.path.normpath(os.path.join(root, d))
|
|
128
|
+
if RepoWalk.isRepo(path):
|
|
129
|
+
yield path
|
|
130
|
+
break
|
|
131
|
+
|
|
132
|
+
def repoType(dirs, files) -> int:
|
|
133
|
+
'''0 None, 1 Normal, 2 Submodule'''
|
|
134
|
+
if '.git' in dirs:
|
|
135
|
+
return 1
|
|
136
|
+
if '.git' in files:
|
|
137
|
+
return 2
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
def isRepo(directory) -> int:
|
|
141
|
+
for _, dirs, files in os.walk(directory):
|
|
142
|
+
return RepoWalk.repoType(dirs, files) != 0
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
# 参考文档
|
|
146
|
+
# - [GitPython doc](https://gitpython.readthedocs.io/en/stable/)
|
|
147
|
+
# - [How to manage git repositories with Python](https://linuxconfig.org/how-to-manage-git-repositories-with-python)
|
|
148
|
+
class RepoStatus:
|
|
149
|
+
def __init__(self, directory:str):
|
|
150
|
+
self.repo = git.Repo(directory)
|
|
151
|
+
self.status = []
|
|
152
|
+
|
|
153
|
+
class AssetState:
|
|
154
|
+
def __init__(self, x = '', y = '', path = ''):
|
|
155
|
+
self.X = x
|
|
156
|
+
self.Y = y
|
|
157
|
+
self.PATH = path
|
|
158
|
+
self.ORIG_PATH = None
|
|
159
|
+
|
|
160
|
+
def match(self, condition:str='dirty') -> bool:
|
|
161
|
+
'''
|
|
162
|
+
condition:
|
|
163
|
+
- dirty: 存在未提交 或 未跟踪的内容
|
|
164
|
+
- modified: 存在未提交的修改
|
|
165
|
+
- untracked: 存在未跟踪的文件
|
|
166
|
+
'''
|
|
167
|
+
|
|
168
|
+
'''
|
|
169
|
+
XY可能出现的值:
|
|
170
|
+
- ' ' = unmodified
|
|
171
|
+
- 'M' = modified
|
|
172
|
+
- 'A' = added
|
|
173
|
+
- 'D' = deleted
|
|
174
|
+
- 'R' = renamed
|
|
175
|
+
- 'C' = copied
|
|
176
|
+
- 'U' = updated but unmerged
|
|
177
|
+
|
|
178
|
+
X Y Meaning
|
|
179
|
+
-------------------------------------------------
|
|
180
|
+
[AMD] not updated
|
|
181
|
+
M [ MD] updated in index
|
|
182
|
+
A [ MD] added to index
|
|
183
|
+
D deleted from index(git rm)
|
|
184
|
+
R [ MD] renamed in index(git mv)
|
|
185
|
+
C [ MD] copied in index
|
|
186
|
+
[MARC] index and work tree matches
|
|
187
|
+
[ MARC] M work tree changed since index
|
|
188
|
+
[ MARC] D deleted in work tree
|
|
189
|
+
[ D] R renamed in work tree
|
|
190
|
+
[ D] C copied in work tree
|
|
191
|
+
-------------------------------------------------
|
|
192
|
+
D D unmerged, both deleted
|
|
193
|
+
A U unmerged, added by us
|
|
194
|
+
U D unmerged, deleted by them
|
|
195
|
+
U A unmerged, added by them
|
|
196
|
+
D U unmerged, deleted by us
|
|
197
|
+
A A unmerged, both added
|
|
198
|
+
U U unmerged, both modified
|
|
199
|
+
-------------------------------------------------
|
|
200
|
+
? ? untracked
|
|
201
|
+
! ! ignored
|
|
202
|
+
-------------------------------------------------
|
|
203
|
+
'''
|
|
204
|
+
|
|
205
|
+
flags = 0
|
|
206
|
+
if not self.X in ' ?' or self.Y in 'MD':
|
|
207
|
+
flags = 1
|
|
208
|
+
elif self.X == '?' and self.Y == '?':
|
|
209
|
+
flags = 2
|
|
210
|
+
|
|
211
|
+
if condition == 'modified':
|
|
212
|
+
return flags == 1
|
|
213
|
+
elif condition == 'untracked':
|
|
214
|
+
return flags == 2
|
|
215
|
+
elif condition == 'dirty':
|
|
216
|
+
return flags != 0
|
|
217
|
+
else:
|
|
218
|
+
raise RuntimeError(f'Invalid parameter: condition={condition}')
|
|
219
|
+
|
|
220
|
+
def __bool__(self):
|
|
221
|
+
'''返回True, 表示仓库状态是脏的(dirty)'''
|
|
222
|
+
return bool(self.status)
|
|
223
|
+
|
|
224
|
+
def load(self):
|
|
225
|
+
'''
|
|
226
|
+
加载仓库状态
|
|
227
|
+
'''
|
|
228
|
+
|
|
229
|
+
'''
|
|
230
|
+
相当于执行下面的命令:
|
|
231
|
+
git status --porcelain=1 --untracked-files=normal
|
|
232
|
+
|
|
233
|
+
参考:
|
|
234
|
+
https://git-scm.com/docs/git-status
|
|
235
|
+
https://git-scm.com/docs/git-status#_output
|
|
236
|
+
|
|
237
|
+
Short Format:
|
|
238
|
+
- XY PATH
|
|
239
|
+
- XY ORIG_PATH -> PATH
|
|
240
|
+
|
|
241
|
+
XY 是两个字符的状态码, ORIG_PATH 只在 重命名 或 复制 时显示.
|
|
242
|
+
|
|
243
|
+
XY 语法有三种不同模式的状态:
|
|
244
|
+
- 一般模式: 即当合并成功或在合并之外的情况, X表示index的状态, Y表示工作树的状态.
|
|
245
|
+
- 合并模式: 即当发生合并冲突且尚未解决时, X和Y表示相对于共同的祖先, 两个HEAD所引入的状态, PATH 表示未合并.
|
|
246
|
+
- 未跟踪模式: 即当PATH是未跟踪的文件时, X与Y总是相同的, 因为它们index是未知的.
|
|
247
|
+
忽略的文件不会被列出, 除非使用--ignored; 如果是,则忽略的文件将用!!表示。
|
|
248
|
+
'''
|
|
249
|
+
self.status = []
|
|
250
|
+
|
|
251
|
+
# porcelain 易于解析的简单的输出, 类似-s(Short Format)
|
|
252
|
+
# untracked_files 包含详细的未跟踪文件列表
|
|
253
|
+
# - normal - Shows untracked files and directories.
|
|
254
|
+
# - all - Also shows individual files in untracked directories.
|
|
255
|
+
# - no - Show no untracked files.
|
|
256
|
+
proc = self.repo.git.status(porcelain='1', untracked_files='normal', as_process=True)
|
|
257
|
+
|
|
258
|
+
# XY PATH
|
|
259
|
+
# XY ORIG_PATH -> PATH
|
|
260
|
+
for line in proc.stdout:
|
|
261
|
+
line = line.decode('utf-8').rstrip('\n')
|
|
262
|
+
if len(line) == 0:
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
asset = self.AssetState()
|
|
266
|
+
asset.X = line[0:1]
|
|
267
|
+
asset.Y = line[1:2]
|
|
268
|
+
asset.PATH = line[3:]
|
|
269
|
+
|
|
270
|
+
if ' -> ' in asset.PATH:
|
|
271
|
+
match = re.search(r'^"?(.+?)"? -> "?(.+?)"?$', asset.PATH)
|
|
272
|
+
if match is None:
|
|
273
|
+
raise RuntimeError('Unexpected format: ' + line)
|
|
274
|
+
asset.PATH = match.group(2)
|
|
275
|
+
asset.ORIG_PATH = match.group(1)
|
|
276
|
+
elif asset.PATH[0] == '"' and asset.PATH[-1] == '"':
|
|
277
|
+
asset.PATH = asset.PATH[1:-2]
|
|
278
|
+
self.status.append(asset)
|
|
279
|
+
proc.wait()
|
|
280
|
+
return self
|
|
281
|
+
|
|
282
|
+
def match(self, condition:str='dirty') -> bool:
|
|
283
|
+
'''
|
|
284
|
+
condition:
|
|
285
|
+
- modified: 存在未提交的修改
|
|
286
|
+
- untracked: 存在未跟踪的文件
|
|
287
|
+
- dirty: 存在modified或untracked
|
|
288
|
+
- clean: 不存在modified或untracked
|
|
289
|
+
'''
|
|
290
|
+
if condition == 'all':
|
|
291
|
+
return True
|
|
292
|
+
elif condition == 'clean':
|
|
293
|
+
return not self
|
|
294
|
+
elif condition == 'dirty':
|
|
295
|
+
return bool(self)
|
|
296
|
+
|
|
297
|
+
for asset in self.status:
|
|
298
|
+
if asset.match(condition):
|
|
299
|
+
return True
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
def display(self, root:str, level:str='brief'):
|
|
303
|
+
dir = self.repo.working_dir
|
|
304
|
+
dir = os.path.relpath(self.repo.working_dir, root)
|
|
305
|
+
|
|
306
|
+
if level == 'none':
|
|
307
|
+
cprint(dir)
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
cprint(dir, 'green', end=' ')
|
|
311
|
+
cprint(f'({self.repo.active_branch.name})', 'cyan')
|
|
312
|
+
|
|
313
|
+
if level == 'brief':
|
|
314
|
+
modified = []
|
|
315
|
+
untracked = []
|
|
316
|
+
for item in self.status:
|
|
317
|
+
if not item.X in ' ?' or item.Y in 'MD':
|
|
318
|
+
modified.append(item)
|
|
319
|
+
elif item.X == '?' and item.Y == '?':
|
|
320
|
+
untracked.append(item)
|
|
321
|
+
|
|
322
|
+
if not modified and not untracked:
|
|
323
|
+
cprint(f' Clean', 'white')
|
|
324
|
+
else:
|
|
325
|
+
cprint(f' Modified: {len(modified)}, Untracked: {len(untracked)}', 'red')
|
|
326
|
+
|
|
327
|
+
else:
|
|
328
|
+
lastcwd = os.getcwd()
|
|
329
|
+
try:
|
|
330
|
+
os.chdir(self.repo.working_dir)
|
|
331
|
+
if level == 'normal':
|
|
332
|
+
os.system('git status -s --untracked-files=normal --ignore-submodules=all')
|
|
333
|
+
else:
|
|
334
|
+
os.system('git status -b --show-stash --untracked-files=all --ignore-submodules=all --ignored')
|
|
335
|
+
finally:
|
|
336
|
+
os.chdir(lastcwd)
|
|
337
|
+
|
|
338
|
+
class RepoHandler:
|
|
339
|
+
def __init__(self):
|
|
340
|
+
self.success = []
|
|
341
|
+
self.failure = []
|
|
342
|
+
|
|
343
|
+
def execute(cmd:str) -> int:
|
|
344
|
+
code = os.system(cmd)
|
|
345
|
+
if platform.system().lower() != 'windows':
|
|
346
|
+
code >>= 8
|
|
347
|
+
return code
|
|
348
|
+
|
|
349
|
+
def perform(self, repo, args):
|
|
350
|
+
lastcwd = os.getcwd()
|
|
351
|
+
try:
|
|
352
|
+
if args.action == 'bash':
|
|
353
|
+
cprint('')
|
|
354
|
+
cprint(f'> Note that you are running in a new bash...', 'yellow')
|
|
355
|
+
cprint(f'> * Press "CTRL + D" to exit the bash!', 'yellow')
|
|
356
|
+
cprint(f'> * Press "CTRL + C, CTRL + D" to abort the {projectName}!', 'yellow')
|
|
357
|
+
os.chdir(repo.repo.working_dir)
|
|
358
|
+
os.system('bash')
|
|
359
|
+
|
|
360
|
+
elif args.action == 'gui':
|
|
361
|
+
os.chdir(repo.repo.working_dir)
|
|
362
|
+
os.system('git gui')
|
|
363
|
+
|
|
364
|
+
elif args.action == 'run':
|
|
365
|
+
cmd = ' '.join(args.params)
|
|
366
|
+
if '{ab}' in cmd:
|
|
367
|
+
cmd = cmd.replace('{ab}', repo.repo.active_branch.name)
|
|
368
|
+
if '{ActiveBranch}' in cmd:
|
|
369
|
+
cmd = cmd.replace('{ActiveBranch}', repo.repo.active_branch.name)
|
|
370
|
+
if '{RepositoryName}' in cmd:
|
|
371
|
+
cmd = cmd.replace('{RepositoryName}', os.path.basename(repo.repo.working_dir))
|
|
372
|
+
if '{cwd}' in cmd:
|
|
373
|
+
cmd = cmd.replace('{cwd}', args.directory)
|
|
374
|
+
|
|
375
|
+
os.chdir(repo.repo.working_dir)
|
|
376
|
+
repo.code = RepoHandler.execute(cmd)
|
|
377
|
+
if args.debug:
|
|
378
|
+
cprint(f'> Execute: {cmd} -> {repo.code}', 'red' if repo.code else 'yellow')
|
|
379
|
+
if repo.code == 0:
|
|
380
|
+
self.success.append(repo)
|
|
381
|
+
else:
|
|
382
|
+
self.failure.append(repo)
|
|
383
|
+
|
|
384
|
+
except Exception as e:
|
|
385
|
+
traceback.print_exc()
|
|
386
|
+
|
|
387
|
+
finally:
|
|
388
|
+
os.chdir(lastcwd)
|
|
389
|
+
|
|
390
|
+
def report(self, prefix:str=''):
|
|
391
|
+
if self.success or self.failure:
|
|
392
|
+
return prefix + f'Run result: success {len(self.success)}, failure {len(self.failure)}'
|
|
393
|
+
else:
|
|
394
|
+
return ''
|
|
395
|
+
|
|
396
|
+
class PathFilter:
|
|
397
|
+
def __init__(self, filename:str=None) -> None:
|
|
398
|
+
self.patterns = None
|
|
399
|
+
self.filename = filename
|
|
400
|
+
self.load(filename)
|
|
401
|
+
|
|
402
|
+
def __bool__(self):
|
|
403
|
+
return self.patterns is not None
|
|
404
|
+
|
|
405
|
+
def load(self, filename:str) -> bool:
|
|
406
|
+
if not filename:
|
|
407
|
+
return
|
|
408
|
+
self.patterns = []
|
|
409
|
+
with open(filename, 'r') as f:
|
|
410
|
+
for line in f:
|
|
411
|
+
line = line.lstrip(' ').rstrip('\n')
|
|
412
|
+
if not line or line.startswith('#'):
|
|
413
|
+
continue
|
|
414
|
+
self.patterns.append(re.compile(line.rstrip('\n')))
|
|
415
|
+
|
|
416
|
+
def match(self, path) -> bool:
|
|
417
|
+
path = path.replace('\\', '/')
|
|
418
|
+
for item in self.patterns:
|
|
419
|
+
if item.match(path):
|
|
420
|
+
return True
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
def cli():
|
|
424
|
+
parser = argparse.ArgumentParser()
|
|
425
|
+
parser.add_argument('--version', action='store_true')
|
|
426
|
+
parser.add_argument('--debug', action='store', nargs='?', default='disabled')
|
|
427
|
+
|
|
428
|
+
parser.add_argument('-v', '--verbose', action='store_true', default=False)
|
|
429
|
+
parser.add_argument('-l', '--level', action='store', choices=['none', 'brief', 'normal', 'verbose'], default='brief')
|
|
430
|
+
|
|
431
|
+
parser.add_argument('-d', '--directory', action='store', default=os.getcwd())
|
|
432
|
+
parser.add_argument('-r', '--recursive', action='store_true', default=False)
|
|
433
|
+
|
|
434
|
+
parser.add_argument('-f', '--filter', action='store', choices=['all', 'clean', 'dirty', 'modified', 'untracked'], default='dirty')
|
|
435
|
+
parser.add_argument('--blacklist', action='store', default='')
|
|
436
|
+
parser.add_argument('--whitelist', action='store', default='')
|
|
437
|
+
parser.add_argument('--force', action='store_true')
|
|
438
|
+
|
|
439
|
+
parser.add_argument('-a', '--action', action='store', choices=['bash', 'gui', 'run'], default=None)
|
|
440
|
+
parser.add_argument('params', nargs=argparse.REMAINDER)
|
|
441
|
+
|
|
442
|
+
args = parser.parse_args()
|
|
443
|
+
|
|
444
|
+
mapping = {'disabled' : '', None : 'enabled'}
|
|
445
|
+
if args.debug in mapping:
|
|
446
|
+
args.debug = mapping[args.debug]
|
|
447
|
+
if args.debug:
|
|
448
|
+
print(f'> {projectName} args={args}')
|
|
449
|
+
if 'wait' in args.debug:
|
|
450
|
+
input('Wait for debugging and press Enter to continue...')
|
|
451
|
+
|
|
452
|
+
if args.version:
|
|
453
|
+
print(f'{projectName} version {projectVersion} powered by {projectAuthor}')
|
|
454
|
+
print(f'Home: {projectHome}')
|
|
455
|
+
exit(0)
|
|
456
|
+
|
|
457
|
+
# --verbose 优先
|
|
458
|
+
if args.verbose:
|
|
459
|
+
args.level = 'verbose'
|
|
460
|
+
|
|
461
|
+
# 观察
|
|
462
|
+
args.untracked_files = 'normal'
|
|
463
|
+
if args.level == 'verbose':
|
|
464
|
+
args.untracked_files = 'all'
|
|
465
|
+
|
|
466
|
+
args.directory = args.directory.strip(' \'"')
|
|
467
|
+
args.blacklist = args.blacklist.strip(' \'"')
|
|
468
|
+
args.whitelist = args.whitelist.strip(' \'"')
|
|
469
|
+
|
|
470
|
+
if args.blacklist and not os.path.exists(args.blacklist):
|
|
471
|
+
raise RuntimeError(f'Invalid blacklist: {args.blacklist}')
|
|
472
|
+
if args.whitelist and not os.path.exists(args.whitelist):
|
|
473
|
+
raise RuntimeError(f'Invalid whitelist: {args.whitelist}')
|
|
474
|
+
if not args.blacklist and os.path.exists(f'{projectName}.blacklist'):
|
|
475
|
+
args.blacklist = f'{projectName}.blacklist'
|
|
476
|
+
|
|
477
|
+
# 如果白名单被指定, 则使黑名单失效, 类似: --force
|
|
478
|
+
if args.whitelist:
|
|
479
|
+
args.force = True
|
|
480
|
+
if args.force:
|
|
481
|
+
args.blacklist = ''
|
|
482
|
+
args.whitelist = PathFilter(args.whitelist)
|
|
483
|
+
args.blacklist = PathFilter(args.blacklist)
|
|
484
|
+
if args.debug:
|
|
485
|
+
cprint(f'> Blacklist: ' + (f'Valid {{{args.blacklist.filename}}}' if args.blacklist else 'Invalid'), 'yellow')
|
|
486
|
+
cprint(f'> Whitelist: ' + (f'Valid {{{args.whitelist.filename}}}' if args.whitelist else 'Invalid'), 'yellow')
|
|
487
|
+
|
|
488
|
+
ignored = 0
|
|
489
|
+
matched = 0
|
|
490
|
+
handler = RepoHandler()
|
|
491
|
+
for path in RepoWalk(args.directory, args.recursive, debug=args.debug):
|
|
492
|
+
def filter(list, name, reverse=False):
|
|
493
|
+
'''
|
|
494
|
+
返回True表示被忽略, 黑名单匹配的项应该忽略, 而白名单匹配的项则反之, 名单未初始化则不参与过滤.
|
|
495
|
+
matched : reverse 的组合结果如下:
|
|
496
|
+
1 : 0 = 1
|
|
497
|
+
0 : 0 = 0
|
|
498
|
+
1 : 1 = 0
|
|
499
|
+
0 : 1 = 1
|
|
500
|
+
'''
|
|
501
|
+
|
|
502
|
+
if not list:
|
|
503
|
+
return False
|
|
504
|
+
matched = list.match(path)
|
|
505
|
+
if args.debug:
|
|
506
|
+
if 'trace' in args.debug:
|
|
507
|
+
cprint(f'> {name}.match({path}): {matched}', 'yellow' if matched else 'white')
|
|
508
|
+
if matched ^ reverse:
|
|
509
|
+
cprint(f'> ignored repo that {"not in" if reverse else "in"} {name}: {os.path.relpath(path, args.directory)}', 'yellow')
|
|
510
|
+
return matched ^ reverse
|
|
511
|
+
|
|
512
|
+
if filter(args.blacklist, 'blacklist') or filter(args.whitelist, 'whitelist', True):
|
|
513
|
+
ignored += 1
|
|
514
|
+
continue
|
|
515
|
+
|
|
516
|
+
repo = RepoStatus(path)
|
|
517
|
+
if not (args.filter == 'all' and args.level == 'none'):
|
|
518
|
+
repo.load()
|
|
519
|
+
if not repo.match(args.filter):
|
|
520
|
+
ignored += 1
|
|
521
|
+
if args.debug:
|
|
522
|
+
cprint(f'> ignored repo that not match filter "{args.filter}": {os.path.relpath(path, args.directory)}', 'yellow')
|
|
523
|
+
continue
|
|
524
|
+
|
|
525
|
+
matched += 1
|
|
526
|
+
repo.display(args.directory, args.level)
|
|
527
|
+
handler.perform(repo, args)
|
|
528
|
+
|
|
529
|
+
cprint('')
|
|
530
|
+
cprint(f'Walked {matched+ignored} repo, matched: {matched}, ignored: {ignored}{handler.report("; ")}',
|
|
531
|
+
'red' if handler.failure else 'white')
|
|
532
|
+
|
|
533
|
+
if handler.failure:
|
|
534
|
+
cprint('The failed projects are as follows:', 'red')
|
|
535
|
+
for repo in handler.failure:
|
|
536
|
+
cprint(' - ' + os.path.relpath(repo.repo.working_dir, args.directory), 'red')
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def main():
|
|
540
|
+
try:
|
|
541
|
+
cli()
|
|
542
|
+
except KeyboardInterrupt:
|
|
543
|
+
pass
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
if __name__ == '__main__':
|
|
547
|
+
main()
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pygwalk
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: gwalk 是一系列用于管理 Git 仓库的命令行小工具,帮助开发者对大批量的 Git 仓库进行日常维护。
|
|
5
|
+
Author-email: Zero Kwok <zero.kwok@foxmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Project-URL: Homepage, https://github.com/ZeroKwok/gwalk
|
|
8
|
+
Project-URL: Issues, https://github.com/ZeroKwok/gwalk/issues
|
|
9
|
+
Keywords: git,tools
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Requires-Python: >=3.7
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: GitPython
|
|
25
|
+
Requires-Dist: termcolor
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# gwalk
|
|
29
|
+
|
|
30
|
+
`gwalk` 是一系列用于管理 Git 仓库的命令行小工具,帮助开发者对大批量的 Git 仓库进行日常维护。
|
|
31
|
+
|
|
32
|
+
## 安装
|
|
33
|
+
|
|
34
|
+
### 1. pip
|
|
35
|
+
|
|
36
|
+
1. `python -m pip install pygwalk`
|
|
37
|
+
|
|
38
|
+
### 2. build from source
|
|
39
|
+
|
|
40
|
+
1. `git clone https://github.com/ZeroKwok/gwalk.git`
|
|
41
|
+
2. `cd gwalk`
|
|
42
|
+
3. `python -m pip install .`
|
|
43
|
+
|
|
44
|
+
## 使用
|
|
45
|
+
|
|
46
|
+
### 1. gl
|
|
47
|
+
|
|
48
|
+
`gl.py` 是 `git pull` 操作的快捷工具。
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 从远程仓库拉取代码并合并到当前分支, 等价于下面的命令
|
|
52
|
+
# git pull {origin 或 第一个remotes} {当前分支}
|
|
53
|
+
gl
|
|
54
|
+
|
|
55
|
+
# git pull {origin 或 第一个remotes} {当前分支} --rebase
|
|
56
|
+
gl --rebase
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. gcp
|
|
60
|
+
|
|
61
|
+
`gcp.py` 是用于执行 `git commit` 和 `git push` 操作快捷工具。
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# 添加未跟踪的文件以及已修改的文件,并提交到远程仓库, 等价于下面的命令
|
|
65
|
+
# git add -u && git commit -m "fix some bugs" && git push
|
|
66
|
+
gcp "fix some bugs"
|
|
67
|
+
|
|
68
|
+
# 仅推送当前分支到所有远程仓库,不进行提交
|
|
69
|
+
gcp -p
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3. gwalk
|
|
73
|
+
|
|
74
|
+
`gwalk.py` 是 `gwalk` 工具的主要组件,提供了以下功能:
|
|
75
|
+
|
|
76
|
+
- 列出目录下的所有 Git 仓库,支持过滤条件、黑名单、白名单和目录递归。
|
|
77
|
+
- 显示列出的仓库的状态信息,支持输出信息的简短或冗长格式。
|
|
78
|
+
- 在每个列出的仓库中执行一个操作。如运行自定义命令: 类似于子仓库操作 `git submodule foreach 'some command'` 但更加灵活。
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# 列出当前目录下所有的'脏'的 Git 仓库
|
|
82
|
+
gwalk
|
|
83
|
+
|
|
84
|
+
# 递归列出当前目录下所有的 Git 仓库
|
|
85
|
+
gwalk -rf all
|
|
86
|
+
|
|
87
|
+
# 在列出的每个仓库中执行命令: git pull origin
|
|
88
|
+
gwalk -rf all -a "run git pull origin"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 使用技巧
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# 在所有 gwalk 列出的仓库中, 执行 gl 工具(git pull)
|
|
95
|
+
gwalk -rf all -a run gl
|
|
96
|
+
|
|
97
|
+
# 在所有 gwalk 列出的仓库中, 执行 git push 操作 {ab} 表示 当前分支(ActiveBranch)
|
|
98
|
+
gwalk -rf all -a run git push second {ab}
|
|
99
|
+
|
|
100
|
+
# 批量手动处理(交互模式)
|
|
101
|
+
# 在列出的所有 '包含未提交的修改' 的仓库中, 启动一个 bash shell 来接受用户的操作
|
|
102
|
+
gwalk -rf modified --a bash
|
|
103
|
+
|
|
104
|
+
# 批量推送
|
|
105
|
+
# 在列出的所有 '包含未提交的修改 且 不再黑名单中' 的仓库中, 运行 gcp 工具, 推送当前分支到所有远程仓库
|
|
106
|
+
gwalk -rf modified --blacklist gwalk.blacklist --a "gcp -p"
|
|
107
|
+
|
|
108
|
+
# 批量打标签
|
|
109
|
+
# 在列出的所有 白名单 gwalk.whitelist 匹配的仓库中, 运行 git tag v1.5.0
|
|
110
|
+
gwalk -rf all --whitelist gwalk.whitelist -a run git tag v1.5.0
|
|
111
|
+
|
|
112
|
+
# 批量查看目录下所有仓库的最近3次提交
|
|
113
|
+
gwalk -f all -l none -a run "git log --oneline -n3"
|
|
114
|
+
|
|
115
|
+
# 批量替换 origin 远程仓库的地址, 从 github.com 替换成 gitee.com
|
|
116
|
+
# 在所有 gwalk 列出的仓库中, 执行自定义命令
|
|
117
|
+
gwalk -rf all -a run git remote set-url origin `echo \`git remote get-url origin\` | python -c "print(input().replace('github.com', 'gitee.com'))"`
|
|
118
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
gwalk/__init__.py
|
|
5
|
+
gwalk/gapply.py
|
|
6
|
+
gwalk/gcp.py
|
|
7
|
+
gwalk/gl.py
|
|
8
|
+
gwalk/gwalk.py
|
|
9
|
+
pygwalk.egg-info/PKG-INFO
|
|
10
|
+
pygwalk.egg-info/SOURCES.txt
|
|
11
|
+
pygwalk.egg-info/dependency_links.txt
|
|
12
|
+
pygwalk.egg-info/entry_points.txt
|
|
13
|
+
pygwalk.egg-info/requires.txt
|
|
14
|
+
pygwalk.egg-info/top_level.txt
|
|
15
|
+
tests/test_gwalk.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gwalk
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pygwalk"
|
|
7
|
+
authors = [{ name = "Zero Kwok", email = "zero.kwok@foxmail.com" }]
|
|
8
|
+
description = "gwalk 是一系列用于管理 Git 仓库的命令行小工具,帮助开发者对大批量的 Git 仓库进行日常维护。"
|
|
9
|
+
keywords = ["git", "tools"]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.7"
|
|
12
|
+
license = {text = "MIT License"}
|
|
13
|
+
classifiers = [
|
|
14
|
+
# How mature is this project? Common values are
|
|
15
|
+
# 3 - Alpha
|
|
16
|
+
# 4 - Beta
|
|
17
|
+
# 5 - Production/Stable
|
|
18
|
+
"Development Status :: 5 - Production/Stable",
|
|
19
|
+
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Topic :: Software Development :: Build Tools",
|
|
22
|
+
|
|
23
|
+
"Programming Language :: Python",
|
|
24
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.7",
|
|
27
|
+
"Programming Language :: Python :: 3.8",
|
|
28
|
+
"Programming Language :: Python :: 3.9",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"GitPython",
|
|
34
|
+
"termcolor",
|
|
35
|
+
]
|
|
36
|
+
dynamic = ["version"]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.dynamic]
|
|
39
|
+
version = {attr = "gwalk.gwalk.projectVersion"}
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/ZeroKwok/gwalk"
|
|
43
|
+
Issues = "https://github.com/ZeroKwok/gwalk/issues"
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
gl = "gwalk.gl:main"
|
|
47
|
+
gcp = "gwalk.gcp:main"
|
|
48
|
+
gwalk = "gwalk.gwalk:main"
|
|
49
|
+
gapply = "gwalk.gapply:main"
|
pygwalk-0.2.1/setup.cfg
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pytest
|
|
3
|
+
from gwalk.gwalk import RepoStatus, PathFilter
|
|
4
|
+
|
|
5
|
+
class TestRepoStatus:
|
|
6
|
+
def test_asset_state_match(self):
|
|
7
|
+
"""测试 AssetState.match() 方法"""
|
|
8
|
+
state = RepoStatus.AssetState('M', 'M', 'test.txt')
|
|
9
|
+
assert state.match('modified') == True
|
|
10
|
+
assert state.match('untracked') == False
|
|
11
|
+
assert state.match('dirty') == True
|
|
12
|
+
|
|
13
|
+
state = RepoStatus.AssetState('?', '?', 'test.txt')
|
|
14
|
+
assert state.match('modified') == False
|
|
15
|
+
assert state.match('untracked') == True
|
|
16
|
+
assert state.match('dirty') == True
|
|
17
|
+
|
|
18
|
+
state = RepoStatus.AssetState(' ', ' ', 'test.txt')
|
|
19
|
+
assert state.match('modified') == False
|
|
20
|
+
assert state.match('untracked') == False
|
|
21
|
+
assert state.match('dirty') == False
|
|
22
|
+
|
|
23
|
+
class TestPathFilter:
|
|
24
|
+
def test_path_filter_match(self, tmp_path):
|
|
25
|
+
"""测试 PathFilter.match() 方法"""
|
|
26
|
+
# 创建临时的黑名单文件
|
|
27
|
+
blacklist = tmp_path / "test.blacklist"
|
|
28
|
+
blacklist.write_text("""
|
|
29
|
+
# 注释行
|
|
30
|
+
^.+/test1$
|
|
31
|
+
^.+/test2$
|
|
32
|
+
""")
|
|
33
|
+
|
|
34
|
+
filter = PathFilter(str(blacklist))
|
|
35
|
+
assert filter.match('path/to/test1') == True
|
|
36
|
+
assert filter.match('path/to/test2') == True
|
|
37
|
+
assert filter.match('path/to/test3') == False
|
|
38
|
+
|
|
39
|
+
def test_path_filter_empty(self):
|
|
40
|
+
"""测试空的 PathFilter"""
|
|
41
|
+
filter = PathFilter(None)
|
|
42
|
+
assert bool(filter) == False
|