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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,3 @@
1
+ [console_scripts]
2
+ gits = gits.__main__:main
3
+
@@ -0,0 +1 @@
1
+ argcomplete
@@ -0,0 +1,9 @@
1
+ import logging
2
+
3
+ __version__ = "1.0.0"
4
+ __author__ = "yuqiaoyu"
5
+
6
+ __gits_root_dir__ = ".gits"
7
+ __gits_config_file_name__ = "gits_config.json"
8
+
9
+ logging.basicConfig(level=logging.DEBUG, format="%(asctime)s gits %(levelname)s: %(message)s")
@@ -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