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 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
+ ```
@@ -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,5 @@
1
+ [console_scripts]
2
+ gapply = gwalk.gapply:main
3
+ gcp = gwalk.gcp:main
4
+ gl = gwalk.gl:main
5
+ gwalk = gwalk.gwalk:main
@@ -0,0 +1,2 @@
1
+ GitPython
2
+ termcolor
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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