git-sanity 1.0.0__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.
- git-sanity-1.0.0/LICENSE.txt +21 -0
- git-sanity-1.0.0/PKG-INFO +30 -0
- git-sanity-1.0.0/README.md +116 -0
- git-sanity-1.0.0/setup.cfg +4 -0
- git-sanity-1.0.0/setup.py +39 -0
- git-sanity-1.0.0/src/git_sanity.egg-info/PKG-INFO +30 -0
- git-sanity-1.0.0/src/git_sanity.egg-info/SOURCES.txt +18 -0
- git-sanity-1.0.0/src/git_sanity.egg-info/dependency_links.txt +1 -0
- git-sanity-1.0.0/src/git_sanity.egg-info/entry_points.txt +3 -0
- git-sanity-1.0.0/src/git_sanity.egg-info/requires.txt +1 -0
- git-sanity-1.0.0/src/git_sanity.egg-info/top_level.txt +1 -0
- git-sanity-1.0.0/src/gits/__init__.py +9 -0
- git-sanity-1.0.0/src/gits/__main__.py +70 -0
- git-sanity-1.0.0/src/gits/gits_branch.py +38 -0
- git-sanity-1.0.0/src/gits/gits_clone.py +39 -0
- git-sanity-1.0.0/src/gits/gits_init.py +68 -0
- git-sanity-1.0.0/src/gits/gits_push.py +45 -0
- git-sanity-1.0.0/src/gits/gits_switch.py +49 -0
- git-sanity-1.0.0/src/gits/gits_sync.py +29 -0
- git-sanity-1.0.0/src/gits/utils.py +168 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 yuqiaoyu
|
|
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.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: git-sanity
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Manage multiple git repos with sanity
|
|
5
|
+
Home-page: https://github.com/yuqiaoyu/gits
|
|
6
|
+
Author: yuqiaoyu
|
|
7
|
+
Author-email: yu_junqiang@qq.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: git,manage multiple repositories,cui,command-line
|
|
10
|
+
Platform: linux
|
|
11
|
+
Platform: osx
|
|
12
|
+
Platform: win32
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: POSIX
|
|
17
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
18
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
19
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
20
|
+
Classifier: Topic :: Terminals
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Requires-Python: ~=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE.txt
|
|
28
|
+
|
|
29
|
+
UNKNOWN
|
|
30
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# gits
|
|
2
|
+
多Git仓库管理工具
|
|
3
|
+
|
|
4
|
+
## gits 用法:
|
|
5
|
+
```
|
|
6
|
+
usage: gits [-h] [-v] {init,clone,sync,switch,branch,cherry-pick,push} ...
|
|
7
|
+
|
|
8
|
+
options:
|
|
9
|
+
-h, --help show this help message and exit
|
|
10
|
+
-v, --version show program's version number and exit
|
|
11
|
+
|
|
12
|
+
sub-commands:
|
|
13
|
+
{init,clone,sync,switch,branch,cherry-pick,push}
|
|
14
|
+
additional help with sub-command -h
|
|
15
|
+
init initialize the projects
|
|
16
|
+
clone clone repo(s)
|
|
17
|
+
sync sync source project(s)
|
|
18
|
+
switch switch branches
|
|
19
|
+
branch list or delete branches
|
|
20
|
+
cherry-pick TODO:add repo(s)
|
|
21
|
+
push update remote refs along with associated objects
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 一、`gits init` 用法
|
|
25
|
+
```
|
|
26
|
+
usage: gits init [-h] [-u URL] [-d DIRECTORY]
|
|
27
|
+
|
|
28
|
+
Initialize the projects
|
|
29
|
+
|
|
30
|
+
options:
|
|
31
|
+
-h, --help show this help message and exit
|
|
32
|
+
-u URL, --url URL the gits configuration repository
|
|
33
|
+
-d DIRECTORY, --directory DIRECTORY
|
|
34
|
+
the path to init gits configuration repository
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 二、`gits clone` 用法
|
|
38
|
+
```
|
|
39
|
+
usage: gits clone [-h] [-g GROUP]
|
|
40
|
+
|
|
41
|
+
Clone repo(s)
|
|
42
|
+
|
|
43
|
+
options:
|
|
44
|
+
-h, --help show this help message and exit
|
|
45
|
+
-g GROUP, --group GROUP
|
|
46
|
+
group to clone, default all
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 三、`gits sync` 用法
|
|
50
|
+
```
|
|
51
|
+
usage: gits sync [-h] [-g GROUP]
|
|
52
|
+
|
|
53
|
+
Sync source project(s)
|
|
54
|
+
|
|
55
|
+
options:
|
|
56
|
+
-h, --help show this help message and exit
|
|
57
|
+
-g GROUP, --group GROUP
|
|
58
|
+
projects to sync, default all
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 四、`gits switch` 用法
|
|
62
|
+
```
|
|
63
|
+
usage: gits switch [-h] [-g GROUP] [-c NEW_BRANCH_NAME | -b BRANCH_NAME] [remote]
|
|
64
|
+
|
|
65
|
+
Switch branches
|
|
66
|
+
|
|
67
|
+
positional arguments:
|
|
68
|
+
remote remote name to switch branches, default origin
|
|
69
|
+
|
|
70
|
+
options:
|
|
71
|
+
-h, --help show this help message and exit
|
|
72
|
+
-g GROUP, --group GROUP
|
|
73
|
+
group to switch branches, default all
|
|
74
|
+
-c NEW_BRANCH_NAME, --create NEW_BRANCH_NAME
|
|
75
|
+
create a new branch named <new-branch> base on origin/HEAD
|
|
76
|
+
-b BRANCH_NAME, --branch BRANCH_NAME
|
|
77
|
+
branch to switch to
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 五、`gits branch` 用法
|
|
81
|
+
```
|
|
82
|
+
usage: gits branch [-h] [-g GROUP] [-d DELETE | -D FORCE_DELETE] [list]
|
|
83
|
+
|
|
84
|
+
List or delete branches
|
|
85
|
+
|
|
86
|
+
positional arguments:
|
|
87
|
+
list list branch names
|
|
88
|
+
|
|
89
|
+
options:
|
|
90
|
+
-h, --help show this help message and exit
|
|
91
|
+
-g GROUP, --group GROUP
|
|
92
|
+
group to operate, default all
|
|
93
|
+
-d DELETE, --delete DELETE
|
|
94
|
+
delete fully merged branch
|
|
95
|
+
-D FORCE_DELETE, --DELETE FORCE_DELETE
|
|
96
|
+
delete branch (even if not merged)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 六、`cherry-pick` 用法
|
|
100
|
+
待实现
|
|
101
|
+
|
|
102
|
+
### 七、`gits push` 用法
|
|
103
|
+
```
|
|
104
|
+
usage: gits push [-h] [-g GROUP] [-f] remote
|
|
105
|
+
|
|
106
|
+
Update remote refs along with associated objects
|
|
107
|
+
|
|
108
|
+
positional arguments:
|
|
109
|
+
remote remote branch to be pushed
|
|
110
|
+
|
|
111
|
+
options:
|
|
112
|
+
-h, --help show this help message and exit
|
|
113
|
+
-g GROUP, --group GROUP
|
|
114
|
+
group to push, default all
|
|
115
|
+
-f, --force force updates
|
|
116
|
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Setup file for gits.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from setuptools import setup, find_packages
|
|
6
|
+
|
|
7
|
+
setup(
|
|
8
|
+
name="git-sanity",
|
|
9
|
+
version="1.0.0",
|
|
10
|
+
package_dir={"":"src"},
|
|
11
|
+
packages=find_packages(where="src"),
|
|
12
|
+
license="MIT",
|
|
13
|
+
description="Manage multiple git repos with sanity",
|
|
14
|
+
long_description=None,
|
|
15
|
+
long_description_content_type="text/markdown",
|
|
16
|
+
url="https://github.com/yuqiaoyu/gits",
|
|
17
|
+
platforms=["linux", "osx", "win32"],
|
|
18
|
+
keywords=["git", "manage multiple repositories", "cui", "command-line"],
|
|
19
|
+
author="yuqiaoyu",
|
|
20
|
+
author_email="yu_junqiang@qq.com",
|
|
21
|
+
entry_points={"console_scripts": ["gits = gits.__main__:main"]},
|
|
22
|
+
python_requires="~=3.10",
|
|
23
|
+
install_requires=["argcomplete"],
|
|
24
|
+
classifiers=[
|
|
25
|
+
"Development Status :: 4 - Beta",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Operating System :: POSIX",
|
|
29
|
+
"Operating System :: MacOS :: MacOS X",
|
|
30
|
+
"Operating System :: Microsoft :: Windows",
|
|
31
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
32
|
+
"Topic :: Terminals",
|
|
33
|
+
"Topic :: Utilities",
|
|
34
|
+
"Programming Language :: Python :: 3.10",
|
|
35
|
+
"Programming Language :: Python :: 3.11",
|
|
36
|
+
"Programming Language :: Python :: 3.12",
|
|
37
|
+
],
|
|
38
|
+
include_package_data=True,
|
|
39
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: git-sanity
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Manage multiple git repos with sanity
|
|
5
|
+
Home-page: https://github.com/yuqiaoyu/gits
|
|
6
|
+
Author: yuqiaoyu
|
|
7
|
+
Author-email: yu_junqiang@qq.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: git,manage multiple repositories,cui,command-line
|
|
10
|
+
Platform: linux
|
|
11
|
+
Platform: osx
|
|
12
|
+
Platform: win32
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: POSIX
|
|
17
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
18
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
19
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
20
|
+
Classifier: Topic :: Terminals
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Requires-Python: ~=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE.txt
|
|
28
|
+
|
|
29
|
+
UNKNOWN
|
|
30
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
src/git_sanity.egg-info/PKG-INFO
|
|
5
|
+
src/git_sanity.egg-info/SOURCES.txt
|
|
6
|
+
src/git_sanity.egg-info/dependency_links.txt
|
|
7
|
+
src/git_sanity.egg-info/entry_points.txt
|
|
8
|
+
src/git_sanity.egg-info/requires.txt
|
|
9
|
+
src/git_sanity.egg-info/top_level.txt
|
|
10
|
+
src/gits/__init__.py
|
|
11
|
+
src/gits/__main__.py
|
|
12
|
+
src/gits/gits_branch.py
|
|
13
|
+
src/gits/gits_clone.py
|
|
14
|
+
src/gits/gits_init.py
|
|
15
|
+
src/gits/gits_push.py
|
|
16
|
+
src/gits/gits_switch.py
|
|
17
|
+
src/gits/gits_sync.py
|
|
18
|
+
src/gits/utils.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
argcomplete
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gits
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from gits import __version__
|
|
5
|
+
from gits.gits_init import gits_init_impl
|
|
6
|
+
from gits.gits_clone import gits_clone_impl
|
|
7
|
+
from gits.gits_sync import gits_sync_impl
|
|
8
|
+
from gits.gits_switch import gits_switch_impl
|
|
9
|
+
from gits.gits_branch import gits_branch_impl
|
|
10
|
+
from gits.gits_push import gits_push_impl
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
logging.info("New Run ==================================================")
|
|
14
|
+
|
|
15
|
+
gits = argparse.ArgumentParser(
|
|
16
|
+
prog="gits", formatter_class=argparse.RawTextHelpFormatter, description=__doc__
|
|
17
|
+
)
|
|
18
|
+
gits.add_argument(
|
|
19
|
+
"-v", "--version", action="version", version=f"%(prog)s {__version__}"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
subparsers = gits.add_subparsers(
|
|
23
|
+
title="sub-commands", help="additional help with sub-command -h"
|
|
24
|
+
)
|
|
25
|
+
gits_init = subparsers.add_parser("init", description="Initialize the projects", help="initialize the projects")
|
|
26
|
+
gits_init.add_argument("-u", "--url", dest="url", help="the gits configuration repository")
|
|
27
|
+
gits_init.add_argument("-d", "--directory", dest="directory", default=".", help="the path to init gits configuration repository")
|
|
28
|
+
gits_init.set_defaults(func=gits_init_impl)
|
|
29
|
+
|
|
30
|
+
gits_clone = subparsers.add_parser("clone", description="Clone repo(s)", help="clone repo(s)")
|
|
31
|
+
gits_clone.add_argument("-g", "--group", dest="group", default="all", help="group to clone, default all")
|
|
32
|
+
gits_clone.set_defaults(func=gits_clone_impl)
|
|
33
|
+
|
|
34
|
+
gits_sync = subparsers.add_parser("sync", description="Sync source project(s) ", help="sync source project(s)")
|
|
35
|
+
gits_sync.add_argument("-g", "--group", dest="group", default="all", help="projects to sync, default all")
|
|
36
|
+
gits_sync.set_defaults(func=gits_sync_impl)
|
|
37
|
+
|
|
38
|
+
gits_switch = subparsers.add_parser("switch", description="Switch branches", help="switch branches")
|
|
39
|
+
gits_switch.add_argument("remote", nargs='?', default="origin", help="remote name to switch branches, default origin")
|
|
40
|
+
gits_switch.add_argument("-g", "--group", dest="group", default="all", help="group to switch branches, default all")
|
|
41
|
+
gits_switch_meg = gits_switch.add_mutually_exclusive_group()
|
|
42
|
+
gits_switch_meg.add_argument("-c", "--create", dest="new_branch_name", help="create a new branch named <new-branch> base on origin/HEAD")
|
|
43
|
+
gits_switch_meg.add_argument("-b", "--branch", dest="branch_name", help="branch to switch to")
|
|
44
|
+
gits_switch.set_defaults(func=gits_switch_impl)
|
|
45
|
+
|
|
46
|
+
gits_branch = subparsers.add_parser("branch", description="List or delete branches", help="list or delete branches")
|
|
47
|
+
gits_branch.add_argument("list", nargs='?', default="list", help="list branch names")
|
|
48
|
+
gits_branch.add_argument("-g", "--group", dest="group", default="all", help="group to operate, default all")
|
|
49
|
+
gits_branch_meg = gits_branch.add_mutually_exclusive_group()
|
|
50
|
+
gits_branch_meg.add_argument("-d", "--delete", dest="delete", help="delete fully merged branch")
|
|
51
|
+
gits_branch_meg.add_argument("-D", "--DELETE", dest="force_delete", help="delete branch (even if not merged)")
|
|
52
|
+
gits_branch.set_defaults(func=gits_branch_impl)
|
|
53
|
+
|
|
54
|
+
gits_cherry_pick = subparsers.add_parser("cherry-pick", description="TODO:add repo(s)", help="TODO:add repo(s)")
|
|
55
|
+
|
|
56
|
+
gits_push = subparsers.add_parser("push", description="Update remote refs along with associated objects", help="update remote refs along with associated objects")
|
|
57
|
+
gits_push.add_argument("remote", default="remote", help="remote branch to be pushed")
|
|
58
|
+
gits_push.add_argument("-g", "--group", dest="group", default="all", help="group to push, default all")
|
|
59
|
+
gits_push.add_argument("-f", "--force", dest="force", action='store_true', help="force updates")
|
|
60
|
+
gits_push.set_defaults(func=gits_push_impl)
|
|
61
|
+
|
|
62
|
+
args = gits.parse_args()
|
|
63
|
+
logging.debug("command args={}".format(args))
|
|
64
|
+
if "func" in args:
|
|
65
|
+
args.func(args)
|
|
66
|
+
else:
|
|
67
|
+
gits.print_help()
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
main()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from gits.utils import run
|
|
4
|
+
from gits.utils import get_gits_dir
|
|
5
|
+
from gits.utils import load_user_config
|
|
6
|
+
from gits.utils import get_user_config
|
|
7
|
+
from gits.utils import get_projects_by_group
|
|
8
|
+
|
|
9
|
+
def gits_branch_impl(args):
|
|
10
|
+
"""
|
|
11
|
+
Implementation for managing Git branches across multiple projects.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
args: Command-line arguments containing:
|
|
15
|
+
- group (str): Group name to filter projects
|
|
16
|
+
- delete (str): Branch name to delete (non-force)
|
|
17
|
+
- force_delete (str): Branch name to force delete
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
None
|
|
21
|
+
"""
|
|
22
|
+
user_config = load_user_config()
|
|
23
|
+
for project in get_projects_by_group(user_config, args.group):
|
|
24
|
+
project_name = get_user_config(project, "name")
|
|
25
|
+
logging.info("Processing branches for {}...".format(get_user_config(project, "name")))
|
|
26
|
+
logging.debug("Processing project={}".format(project))
|
|
27
|
+
|
|
28
|
+
repo_path = os.path.join(get_gits_dir(), get_user_config(project, "local_path", "."), project_name)
|
|
29
|
+
if not os.path.isdir(repo_path):
|
|
30
|
+
logging.error("the remote {} project branch hasn't been pulled locally yet.".format(project_name))
|
|
31
|
+
exit(1)
|
|
32
|
+
|
|
33
|
+
branch_cmd = ["git", "branch"]
|
|
34
|
+
if args.delete is not None:
|
|
35
|
+
branch_cmd.extend(["-d", args.delete])
|
|
36
|
+
elif args.force_delete is not None:
|
|
37
|
+
branch_cmd.extend(["-D", args.force_delete])
|
|
38
|
+
run(branch_cmd, workspace=repo_path)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from gits.utils import run
|
|
4
|
+
from gits.utils import get_gits_dir
|
|
5
|
+
from gits.utils import load_user_config
|
|
6
|
+
from gits.utils import get_user_config
|
|
7
|
+
from gits.utils import get_projects_by_group
|
|
8
|
+
|
|
9
|
+
def gits_clone_impl(args):
|
|
10
|
+
"""
|
|
11
|
+
Implements the gits clone operation for multiple projects based on user configuration
|
|
12
|
+
|
|
13
|
+
This function:
|
|
14
|
+
1. Loads user configuration from arguments
|
|
15
|
+
2. Retrieves all projects belonging to the specified group
|
|
16
|
+
3. Clones each project using git clone with configured parameters
|
|
17
|
+
4. Handles optional forward_to_git parameters
|
|
18
|
+
5. Uses configured workspace or current directory as default
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
args: Command-line arguments containing group name and other configuration
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
None (performs side effects by cloning repositories)
|
|
25
|
+
"""
|
|
26
|
+
user_config = load_user_config()
|
|
27
|
+
for project in get_projects_by_group(user_config, args.group):
|
|
28
|
+
logging.info("Cloning {}...".format(get_user_config(project, "name")))
|
|
29
|
+
logging.debug("Cloning project={}".format(project))
|
|
30
|
+
|
|
31
|
+
clone_cmd = ["git", "clone", get_user_config(project, "url"), "-b", get_user_config(project, "branch")]
|
|
32
|
+
clone_cmd.extend(get_user_config(project, "forward_to_git.clone", []))
|
|
33
|
+
|
|
34
|
+
workspace = os.path.join(get_gits_dir(), get_user_config(project, "local_path", "."))
|
|
35
|
+
result = run(clone_cmd, workspace=workspace)
|
|
36
|
+
if result.returncode == 0:
|
|
37
|
+
for remote in get_user_config(project, "additional_remote", []):
|
|
38
|
+
for remote_name, remote_url in remote.items():
|
|
39
|
+
run(["git", "remote", "add", remote_name, remote_url], workspace=os.path.join(workspace, get_user_config(project, "name")))
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from gits import __gits_root_dir__
|
|
5
|
+
from gits import __gits_config_file_name__
|
|
6
|
+
from gits.utils import run
|
|
7
|
+
|
|
8
|
+
default_config = {
|
|
9
|
+
"this_is_a_comment_example": "<!--JSON-style comment markers (similar to HTML comments)-->",
|
|
10
|
+
"working_branch": "<!--change via the `gits switch` command, default=None-->",
|
|
11
|
+
"projects": [
|
|
12
|
+
{
|
|
13
|
+
"name": "<!--project's name-->",
|
|
14
|
+
"url": "<!--project's repo url>",
|
|
15
|
+
"branch": "<!--branch's name-->",
|
|
16
|
+
"local_path": "<!--local path to clone repo, default=.>",
|
|
17
|
+
"additional_remote": [
|
|
18
|
+
{"<!--remote's name-->": "<!--remote's url-->"}
|
|
19
|
+
],
|
|
20
|
+
"forward_to_git": {
|
|
21
|
+
"clone": ["<!--command args of git clone, eg:--depth=1-->"]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"groups": [
|
|
26
|
+
{
|
|
27
|
+
"group_name": "all",
|
|
28
|
+
"projects": [
|
|
29
|
+
"<!--project_name, default=[]-->"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def gits_init_impl(args):
|
|
36
|
+
"""
|
|
37
|
+
Initialize a gits repository with optional URL or local configuration
|
|
38
|
+
|
|
39
|
+
Parameters:
|
|
40
|
+
- args: Command-line arguments object containing:
|
|
41
|
+
* directory: Target directory for repository
|
|
42
|
+
* URL: Optional remote repository URL for cloning
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
- None (exits with status code 0 on success, 1 on failure)
|
|
46
|
+
"""
|
|
47
|
+
if not os.path.isdir(args.directory):
|
|
48
|
+
logging.error("the value of -d/--directory is not a valid path: {}".format(args.directory))
|
|
49
|
+
exit(1)
|
|
50
|
+
|
|
51
|
+
gits_repo_path = os.path.join(args.directory, __gits_root_dir__)
|
|
52
|
+
if os.path.isdir(gits_repo_path):
|
|
53
|
+
logging.error("reinitialized existing gits repository in {}".format(args.directory))
|
|
54
|
+
exit(0)
|
|
55
|
+
|
|
56
|
+
os.mkdir(gits_repo_path)
|
|
57
|
+
if args.url and run(["wget", args.url], workspace=gits_repo_path, capture_output=True).returncode == 0:
|
|
58
|
+
exit(0)
|
|
59
|
+
elif args.url:
|
|
60
|
+
os.rmdir(gits_repo_path)
|
|
61
|
+
logging.error("failed to initialize gits with {}".format(args.url))
|
|
62
|
+
exit(1)
|
|
63
|
+
else:
|
|
64
|
+
gits_config_file_path = os.path.join(gits_repo_path, __gits_config_file_name__)
|
|
65
|
+
with open(gits_config_file_path, "w", encoding="utf-8") as f:
|
|
66
|
+
json.dump(default_config, f, indent=4, ensure_ascii=False)
|
|
67
|
+
logging.warn("default config file created at {}. Customize as needed.".format(gits_config_file_path))
|
|
68
|
+
exit(0)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from gits.utils import run
|
|
4
|
+
from gits.utils import get_gits_dir
|
|
5
|
+
from gits.utils import load_user_config
|
|
6
|
+
from gits.utils import get_user_config
|
|
7
|
+
from gits.utils import get_projects_by_group
|
|
8
|
+
|
|
9
|
+
def gits_push_impl(args):
|
|
10
|
+
user_config = load_user_config()
|
|
11
|
+
working_branch = get_user_config(user_config, "working_branch")
|
|
12
|
+
if not working_branch:
|
|
13
|
+
logging.error("working_branch is not set. Please set it via `gits switch` command.")
|
|
14
|
+
exit(1)
|
|
15
|
+
|
|
16
|
+
for project in get_projects_by_group(user_config, args.group):
|
|
17
|
+
project_name = get_user_config(project, "name")
|
|
18
|
+
logging.info("Pushing {}...".format(get_user_config(project, "name")))
|
|
19
|
+
logging.debug("Pushing project={}".format(project))
|
|
20
|
+
|
|
21
|
+
repo_path = os.path.join(get_gits_dir(), get_user_config(project, "local_path", "."), project_name)
|
|
22
|
+
if not os.path.isdir(repo_path):
|
|
23
|
+
logging.error("the remote {} project branch hasn't been pulled locally yet.".format(project_name))
|
|
24
|
+
exit(1)
|
|
25
|
+
|
|
26
|
+
current_branch = run(["git", "branch", "--show-current"], workspace=repo_path, capture_output=True).stdout.strip()
|
|
27
|
+
if not current_branch:
|
|
28
|
+
logging.error("failed to get current branch for repo at {}".format(repo_path))
|
|
29
|
+
exit(1)
|
|
30
|
+
elif current_branch != working_branch:
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
check_result = run(["git", "log", "origin/{}..HEAD".format(get_user_config(project, "branch"))], workspace=repo_path, capture_output=True)
|
|
34
|
+
if check_result.returncode != 0 or check_result.stderr.strip():
|
|
35
|
+
logging.error("failed to check for commits to push for repo at {}".format(repo_path))
|
|
36
|
+
exit(1)
|
|
37
|
+
elif not check_result.stdout.strip():
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
push_cmd = ["git", "push", args.remote, current_branch]
|
|
41
|
+
if args.force:
|
|
42
|
+
push_cmd.append("--force")
|
|
43
|
+
result = run(push_cmd, workspace=repo_path)
|
|
44
|
+
if result.returncode != 0:
|
|
45
|
+
exit(1)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from gits.utils import run
|
|
4
|
+
from gits.utils import get_gits_dir
|
|
5
|
+
from gits.utils import load_user_config
|
|
6
|
+
from gits.utils import get_user_config
|
|
7
|
+
from gits.utils import get_projects_by_group
|
|
8
|
+
from gits.utils import update_user_config
|
|
9
|
+
|
|
10
|
+
def gits_switch_impl(args):
|
|
11
|
+
"""
|
|
12
|
+
Implements the branch switching functionality for multiple projects based on group configuration.
|
|
13
|
+
|
|
14
|
+
Parameters:
|
|
15
|
+
- args: Command-line arguments containing:
|
|
16
|
+
- group: Name of the group to switch branches for
|
|
17
|
+
- new_branch_name: Optional name for the new branch to create (if provided)
|
|
18
|
+
- branch_name: Optional name of the existing branch to switch to (if provided)
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
- None (exits with error code 1 on failure)
|
|
22
|
+
"""
|
|
23
|
+
user_config = load_user_config()
|
|
24
|
+
projects_of_group = get_projects_by_group(user_config, args.group)
|
|
25
|
+
if not projects_of_group:
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
for project in projects_of_group:
|
|
29
|
+
project_name = get_user_config(project, "name")
|
|
30
|
+
logging.info("Switching branch for {}...".format(project_name))
|
|
31
|
+
logging.debug("Switching project={}".format(project))
|
|
32
|
+
|
|
33
|
+
if args.new_branch_name is not None:
|
|
34
|
+
switch_cmd = ["git", "switch", "-c", args.new_branch_name, "origin/{}".format(get_user_config(project, "branch"))]
|
|
35
|
+
elif args.branch_name is not None:
|
|
36
|
+
switch_cmd = ["git", "switch", "{}".format(args.branch_name)]
|
|
37
|
+
else:
|
|
38
|
+
logging.error("No branch name provided to switch for {}.".format(project_name))
|
|
39
|
+
exit(1)
|
|
40
|
+
|
|
41
|
+
repo_path = os.path.join(get_gits_dir(), get_user_config(project, "local_path", "."), project_name)
|
|
42
|
+
if not os.path.isdir(repo_path):
|
|
43
|
+
logging.error("the remote {} project branch hasn't been pulled locally yet.".format(project_name))
|
|
44
|
+
exit(1)
|
|
45
|
+
if run(switch_cmd, workspace=repo_path).returncode != 0:
|
|
46
|
+
exit(1)
|
|
47
|
+
else:
|
|
48
|
+
update_user_config("working_branch", args.new_branch_name if args.new_branch_name is not None else args.branch_name)
|
|
49
|
+
return
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from gits.utils import run
|
|
4
|
+
from gits.utils import get_gits_dir
|
|
5
|
+
from gits.utils import load_user_config
|
|
6
|
+
from gits.utils import get_user_config
|
|
7
|
+
from gits.utils import get_projects_by_group
|
|
8
|
+
|
|
9
|
+
def gits_sync_impl(args):
|
|
10
|
+
user_config = load_user_config()
|
|
11
|
+
for project in get_projects_by_group(user_config, args.group):
|
|
12
|
+
project_name = get_user_config(project, "name")
|
|
13
|
+
logging.info("Syncing source for {}...".format(get_user_config(project, "name")))
|
|
14
|
+
logging.debug("Syncing project={}".format(project))
|
|
15
|
+
|
|
16
|
+
repo_path = os.path.join(get_gits_dir(), get_user_config(project, "local_path", "."), project_name)
|
|
17
|
+
if not os.path.isdir(repo_path):
|
|
18
|
+
logging.error("the remote {} project branch hasn't been pulled locally yet.".format(project_name))
|
|
19
|
+
exit(1)
|
|
20
|
+
|
|
21
|
+
fetch_cmd = ["git", "fetch", "origin"]
|
|
22
|
+
if run(["git", "rev-parse", "--is-shallow-repository"], workspace=repo_path, capture_output=True).stdout == "True":
|
|
23
|
+
fetch_cmd.append("--unshallow")
|
|
24
|
+
if run(fetch_cmd, workspace=repo_path).returncode != 0:
|
|
25
|
+
exit(1)
|
|
26
|
+
|
|
27
|
+
rebase_cmd = ["git", "rebase", "origin/{}".format(get_user_config(project, "branch"))]
|
|
28
|
+
if run(rebase_cmd, workspace=repo_path).returncode != 0:
|
|
29
|
+
exit(1)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from gits import __gits_root_dir__
|
|
6
|
+
from gits import __gits_config_file_name__
|
|
7
|
+
|
|
8
|
+
def run(command, workspace="./", env=os.environ, capture_output=False):
|
|
9
|
+
"""
|
|
10
|
+
Execute an external command and return its execution result
|
|
11
|
+
|
|
12
|
+
This function uses subprocess.run() to execute the specified command,
|
|
13
|
+
supporting execution in a specified working directory and environment,
|
|
14
|
+
with the ability to capture command output and error messages.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
command (str/list): Command string or command list to execute (e.g., ["ls", "-l"])
|
|
18
|
+
workspace (str): Working directory path for command execution, defaults to current directory(".")
|
|
19
|
+
env (dict): Environment variables dictionary, defaults to os.environ (current process environment)
|
|
20
|
+
capture_output (bool): Whether to capture command's standard output and error output, defaults to False
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
- When capture_output is True, returns tuple: (stdout_str, stderr_str)
|
|
24
|
+
- When capture_output is False, returns boolean: whether command executed successfully (returncode == 0)
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
>>> run(["ls", "-l"], capture_output=True)
|
|
28
|
+
('total 8\n-rw-r--r-- 1 user group 1234 Jan 1 12:34 file.txt', '')
|
|
29
|
+
|
|
30
|
+
>>> run("ls -l", capture_output=False)
|
|
31
|
+
True
|
|
32
|
+
"""
|
|
33
|
+
logging.debug("gits (cwd:{}) Running: {}".format(workspace, command))
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
command,
|
|
36
|
+
cwd=workspace,
|
|
37
|
+
env=env,
|
|
38
|
+
text=True,
|
|
39
|
+
capture_output=capture_output
|
|
40
|
+
)
|
|
41
|
+
logging.debug("gits result: {}".format(result))
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_gits_dir(start_path=None):
|
|
46
|
+
if start_path is None:
|
|
47
|
+
start_path = os.getcwd()
|
|
48
|
+
|
|
49
|
+
current_path = os.path.abspath(os.path.expanduser(start_path))
|
|
50
|
+
while True:
|
|
51
|
+
gits_path = os.path.join(current_path, __gits_root_dir__)
|
|
52
|
+
if os.path.isdir(gits_path):
|
|
53
|
+
return current_path
|
|
54
|
+
|
|
55
|
+
parent_path = os.path.dirname(current_path)
|
|
56
|
+
if parent_path == current_path:
|
|
57
|
+
break
|
|
58
|
+
current_path = parent_path
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_user_config():
|
|
63
|
+
gits_dir = get_gits_dir()
|
|
64
|
+
if gits_dir is None:
|
|
65
|
+
logging.error("cannot find gits repository from current path: {}".format(os.getcwd()))
|
|
66
|
+
exit(1)
|
|
67
|
+
|
|
68
|
+
gits_config_file_path = os.path.join(gits_dir, __gits_root_dir__, __gits_config_file_name__)
|
|
69
|
+
if os.path.exists(gits_config_file_path):
|
|
70
|
+
with open(gits_config_file_path, "r", encoding="utf-8") as f:
|
|
71
|
+
user_config = json.load(f)
|
|
72
|
+
return user_config
|
|
73
|
+
else:
|
|
74
|
+
logging.error("{} does not exist.".format(gits_config_file_path))
|
|
75
|
+
exit(1)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# JSON-style comment markers (similar to HTML comments)
|
|
79
|
+
json_comment_prefix = "<!--"
|
|
80
|
+
json_comment_suffix = "-->"
|
|
81
|
+
def get_user_config(user_config, field_path, default=None):
|
|
82
|
+
"""
|
|
83
|
+
Safely retrieves a nested configuration value from a dictionary using dot notation,
|
|
84
|
+
with support for commented-out values.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
user_config (dict): Configuration dictionary to search through
|
|
88
|
+
field_path (str): Dot-separated path to the desired field (e.g. 'parent.child')
|
|
89
|
+
default: Default value to return if path doesn't exist or is commented
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The found value if path exists and isn't commented, otherwise returns default.
|
|
93
|
+
Returns None if no default is specified.
|
|
94
|
+
"""
|
|
95
|
+
keys = field_path.split('.')
|
|
96
|
+
current = user_config
|
|
97
|
+
for key in keys:
|
|
98
|
+
if isinstance(current, dict) and key in current:
|
|
99
|
+
current = current[key]
|
|
100
|
+
elif default is None:
|
|
101
|
+
logging.error("there is no key({}) in user_config({})".format(field_path, user_config))
|
|
102
|
+
else:
|
|
103
|
+
return default
|
|
104
|
+
|
|
105
|
+
if isinstance(current, str) and current.startswith(json_comment_prefix) and current.endswith(json_comment_suffix):
|
|
106
|
+
if default is None:
|
|
107
|
+
logging.error("there is no value of key({}) in user_config({})".format(field_path, user_config))
|
|
108
|
+
else:
|
|
109
|
+
return default
|
|
110
|
+
else:
|
|
111
|
+
return current
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_projects_by_group(user_config, group_name):
|
|
115
|
+
"""
|
|
116
|
+
Retrieve all projects belonging to a specified group from user configuration
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
user_config (dict): Dictionary containing user configuration with 'projects' and 'groups'
|
|
120
|
+
group_name (str): Name of the group to filter projects by
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
list: List of project dictionaries that belong to the specified group.
|
|
124
|
+
Returns all projects if group_name is "all"
|
|
125
|
+
"""
|
|
126
|
+
all_projects = get_user_config(user_config, "projects", default=[])
|
|
127
|
+
if group_name == "all":
|
|
128
|
+
return all_projects
|
|
129
|
+
|
|
130
|
+
result_projects = []
|
|
131
|
+
for group in get_user_config(user_config, "groups", default=[]):
|
|
132
|
+
if get_user_config(group, "group_name") != group_name:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
target_projects = get_user_config(group, "projects", default=[])
|
|
136
|
+
for project in all_projects:
|
|
137
|
+
if get_user_config(project, "name", default="") in target_projects:
|
|
138
|
+
result_projects.append(project)
|
|
139
|
+
|
|
140
|
+
return result_projects
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def update_user_config(key_path, new_value):
|
|
144
|
+
if isinstance(key_path, str):
|
|
145
|
+
keys = key_path.split('.')
|
|
146
|
+
else:
|
|
147
|
+
keys = key_path
|
|
148
|
+
|
|
149
|
+
user_config = load_user_config()
|
|
150
|
+
current = user_config
|
|
151
|
+
for key in keys[:-1]:
|
|
152
|
+
if isinstance(current, dict) and key in current:
|
|
153
|
+
current = current[key]
|
|
154
|
+
else:
|
|
155
|
+
logging.error("key_path({}) does not exist".format(key_path))
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
final_key = keys[-1]
|
|
159
|
+
if isinstance(current, dict) and final_key in current:
|
|
160
|
+
current[final_key] = new_value
|
|
161
|
+
else:
|
|
162
|
+
logging.error("key_path({}) does not exist".format(key_path))
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
gits_config_file_path = os.path.join(get_gits_dir(), __gits_root_dir__, __gits_config_file_name__)
|
|
166
|
+
with open(gits_config_file_path, "w", encoding="utf-8") as f:
|
|
167
|
+
json.dump(user_config, f, indent=4, ensure_ascii=False)
|
|
168
|
+
return True
|