|
- #!/usr/bin/env python3
- # -*- coding: utf-8; mode: python; tab-width: 4; indent-tabs-mode: nil -*-
- # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8
- #
- # THIS FILE IS PART OF adtools PROJECT
- #
- # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
- # Version 2, December 2004
- #
- # Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
- #
- # Everyone is permitted to copy and distribute verbatim or modified
- # copies of this license document, and changing it is allowed as long
- # as the name is changed.
- #
- # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
- # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
- #
- # 0. You just DO WHAT THE FUCK YOU WANT TO.
-
- import os
- import sys
- import json
- import datetime
- import shutil
- import argparse
- from urllib.parse import quote_plus, urlencode, urlparse
- from functools import cached_property
-
- import crayons
-
- from .__init__ import __version__
- from .adutil import AdUtil, WorkspaceHelper, Logger, D, W, E
-
- __author__ = ['"anjingyu" <anjingyu_ws@foxmail.com>']
-
- RELEASE_FILE_NAME = "RELEASE.md"
-
-
- class AdGitHelper:
- """A light weight Git wrapper for using easily.
- The basic syntax for git sub-command:
- 1. The function name indicates sub-command, replace the `-` with `_`.
- i.e. git show-branches -> git_helper.show_branches()
- 2. All the list arguments will be regarded as arguments of sub-command.
- i.e. git clone git@git.mapbar.com:AD/ad.git -> git_helper.clone('git@git.mapbar.com:AD/ad.git')
- 3. All the dict arguments will be parsed use the following rules:
- __ -> --
- _v=None -> -v
- __version=None -> --version
- __depth_=1 -> --depth=1
- __depth=1 -> --depth 1
- NOTE: All the dict arguments will be ahead of list arguments"""
-
- def __init__(self, git=None, dry_run=False):
- self.__git = os.path.abspath(git) if git else "git"
- self.__dry_run = dry_run
-
- def command(self, cmd, *args, **kwargs):
- """Parse and run the git sub-command"""
- nargs = list()
- kargs = list()
- if kwargs:
- for key, val in kwargs.items():
- key_cvrt = key.strip().replace("_", "-")
- if key_cvrt.endswith("-") and key_cvrt != "--":
- if val:
- kargs.append("%s=%s" % (key_cvrt[:-1], val))
- else:
- kargs.append(key_cvrt[:-1])
- else:
- if val:
- kargs.append("%s %s" % (key_cvrt, val))
- else:
- kargs.append(key_cvrt)
- nargs.extend(kargs)
- if args:
- nargs.extend(args)
- cmds = "%s %s %s" % (self.__git, cmd, " ".join(nargs) if nargs else "")
- cmds = cmds.strip()
- try:
- if not self.__dry_run:
- rc, rs = AdUtil.run_command(cmds, True)
- return (rc, rs)
- else:
- return (0, "Dry running...")
- except Exception:
- return (-1, "Invalid command")
-
- def __getattr__(self, attr_str):
- def func(*list_params, **dict_params):
- cmd_list = ["init", "clone"]
- cmd = attr_str.replace("_", "-")
- if cmd not in cmd_list and not self.is_git_dir():
- return (-100, "Not a valid git repo")
- return self.command(cmd, *list_params, **dict_params)
-
- return func
-
- def __setattr__(self, s, v):
- self.__dict__[s] = v
-
- def is_git_dir(self, cur_dir="."):
- with AdUtil.chdir(cur_dir):
- code, _ = self.command("rev-parse", "--git-dir")
- if code == 0:
- return True
- return False
-
- def current_branch(self, cur_dir="."):
- with AdUtil.chdir(cur_dir):
- code, branch = self.command("rev-parse", "--abbrev-ref", "HEAD")
- if code != 0:
- W(crayons.yellow(f"Failed to run command: code<{code}>\n{branch}"))
- return None
-
- return branch.strip()
-
- def head_log_info(self, cur_dir="."):
- with AdUtil.chdir(cur_dir):
- ver = "UNKNOWN"
- code, version_info = self.command(
- "log", "-1", '--format="Commit: %H\nAuthor: %aN <%aE>\nDateTime: %aD"'
- )
- if code != 0:
- W(crayons.yellow(f"Failed to get version information in <{cur_dir}>"))
- else:
- ver = version_info
- return ver
-
- def url(self, cur_dir="."):
- with AdUtil.chdir(cur_dir):
- code, branches = self.remote(_v=None)
- if code != 0:
- E(crayons.red(f"Failed to get remote url: code<{code}>\n{branches}"))
- return None
-
- for url_line in branches.split("\n"):
- url = url_line.split()[1].strip()
- return url
-
- def has_branch(self, branch_name, cur_dir="."):
- with AdUtil.chdir(cur_dir):
- # For tags -> refs/tags/, for remote branches -> refs/remotes/origin/
- code, branch = self.command(
- "rev-parse", "--verify", f"refs/heads/{branch_name}"
- )
- return code == 0
-
- def has_remote_branch(self, branch_name, repo='origin', cur_dir="."):
- with AdUtil.chdir(cur_dir):
- code, branch = self.command(
- "rev-parse", "--verify", f"refs/remotes/{repo}/{branch_name}"
- )
- return code == 0
-
- def get_version(self, branch_name="HEAD", cur_dir="."):
- with AdUtil.chdir(cur_dir):
- info = ""
- is_tag = False
-
- # Check whether current commit is a tag(or a list of tags)
- # Always retrieve the latest one.
- code, tags = self.tag("HEAD", __contains=None)
- if code == 0 and tags:
- tags = [t.strip() for t in tags.split()]
- tags.sort(reverse=True)
- info = tags[0]
- is_tag = True
-
- if not info:
- # Check whether current branch is a development branch, named as x.x.x
- code, rbranch = self.rev_parse("HEAD@{u}", __abbrev_ref=None)
- if code == 0:
- info = rbranch.split("/")[-1]
- else:
- # No upstream configured for current local branch,
- # so just use local branch name.
- info = self.current_branch()
-
- # If failed to get branch and tag, just statistics the count of commits.
- if not info:
- info = "0.0.0"
- # Get the description of version
- code, revision = self.rev_list(branch_name, __count=None)
- if code != 0:
- W(
- crayons.yellow(
- f"Failed to get revision from commit record in branch<{branch_name}>: {revision}"
- )
- )
- else:
- info = "{}.{}".format(info, revision)
-
- return info, is_tag
-
- def get_revision(self, branch_name="HEAD", cur_dir="."):
- """
- {
- "date": "Sun, 27 Sep 2020 23:50:17 +0800",
- "commit": "1f845d066b96181a7f7a8f6429086674f53d2c42",
- "short_commit": "1f845d0",
- "author": "author: author@email.com",
- "version": "3.18.2.88",
- "istag": true
- }
-
- The four segments version indicates it is not a tag,
- the three segments verions indicates it is a tag here.
-
- All the versions are started with 0.0.0, if there is no any branch yet,
- just assume the version is started with 0.0.0.x
- """
- ver_obj = {}
- with AdUtil.chdir(cur_dir):
- info, is_tag = self.get_version(branch_name, cur_dir)
-
- code, version_info = self.command(
- "log",
- "-1",
- r'--format="{\"date\": \"%aD\", \"commit\": \"%H\", \"short_commit\": \"%h\", \"author\": \"%aN: %aE\", \"version\": \"\"}"',
- )
- if code != 0:
- W(crayons.yellow(f"Failed to get revision information in <{cur_dir}>"))
- else:
- ver_obj = json.loads(version_info)
- ver_obj["version"] = info.strip()
- ver_obj["istag"] = is_tag
- return ver_obj
-
-
- class AdGit:
- def __init__(self):
- self.ws_helper = WorkspaceHelper()
-
- @cached_property
- def commands(self):
- cmds = [
- ("add", "add file contents to index", self.__common_git_cmd),
- (
- "checkout",
- "checkout branch or paths to working tree",
- self.__common_git_cmd,
- ),
- (
- "clean",
- "remove untracked files from working tree",
- self.__common_git_cmd,
- ),
- ("clone", "clone repository into new directory", self.__common_git_cmd),
- ("commit", "record changes to repository", self.__common_git_cmd),
- (
- "fetch",
- "download objects and refs from another repository",
- self.__common_git_cmd,
- ),
- ("log", "show commit logs", self.__common_git_cmd),
- (
- "ls-files",
- "information about files in index/working directory",
- self.__common_git_cmd,
- ),
- (
- "ls-remote",
- "show references in a remote repository",
- self.__common_git_cmd,
- ),
- ("ls-tree", "list contents of a tree object", self.__common_git_cmd),
- (
- "prune",
- "prune all unreachable objects from the object database",
- self.__common_git_cmd,
- ),
- (
- "pull",
- "fetch from and merge with another repository or local branch",
- self.__common_git_cmd,
- ),
- (
- "push",
- "update remote refs along with associated objects",
- self.__common_git_cmd,
- ),
- (
- "rebase",
- "forward-port local commits to the updated upstream head",
- self.__common_git_cmd,
- ),
- (
- "merge",
- "Join two or more development histories together",
- self.__common_git_cmd,
- ),
- ("remote", "manage set of tracked repositories", self.__common_git_cmd),
- ("reset", "reset current HEAD to specified state", self.__common_git_cmd),
- ("revert", "revert existing commits", self.__common_git_cmd),
- (
- "stash",
- "stash away changes to dirty working directory",
- self.__common_git_cmd,
- ),
- ("status", "show working-tree status", self.__common_git_cmd),
- (
- "submodule",
- "initialize, update, or inspect submodules",
- self.__common_git_cmd,
- ),
- ("branch", "list, create, or delete branches", self.__common_git_cmd),
- (
- "tag",
- "create, list, delete or verify tag object signed with GPG",
- self.__common_git_cmd,
- ),
- ("rbranch", "remote branch operation", self.__special_git_cmd),
- ("rtag", "remote tag operation", self.__special_git_cmd),
- ]
-
- return cmds
-
- def __do_pull(self, git_helper):
- ws = self.ws_helper.current_ws_dir
-
- projs_num = len(cfg_json["repos"])
- i = 0
- fmt = "{{:0>{}".format(len(str(projs_num))) + "d}"
- fmt = "[" + fmt + "/{}] "
- for name, val in cfg_json["repos"].items():
- i += 1
- print(crayons.blue(fmt.format(i, projs_num)), end="")
- if os.path.exists(name):
- with AdUtil.chdir(name):
- if git_helper.pull()[0] == 0:
- print(
- crayons.blue(f"Pulling project: {name: <20}"),
- crayons.green("[√]"),
- )
- else:
- print(
- crayons.blue(f"Pulling project: {name: <20}"),
- crayons.red("[×]"),
- )
-
- def __common_git_cmd(self, git_helper, args, _extra_args):
- sub_cmd = args.sub_cmd
- extra_args = list()
-
- extra_args.extend(_extra_args)
-
- # If current directory is a repository or the user are doing clone action
- if args.sub_cmd == "clone" or os.path.exists(".git"):
- subcmd = "{} {}".format(args.sub_cmd, " ".join(extra_args)).strip()
- code, ret = git_helper.command(subcmd)
- if code != 0:
- print(crayons.red("Failed to run command <{}>\n{}".format(subcmd, ret)))
- else:
- print(crayons.blue(ret))
- else:
- print(
- crayons.blue(
- f"Processing repositories in workspace:",
- crayons.yellow(self.ws_helper.current_ws_name),
- )
- )
-
- ws = self.ws_helper.current_ws_dir
- for name in os.listdir(ws):
- dirn = os.path.join(ws, name)
-
- if not os.path.isdir(dirn):
- continue
-
- print(
- crayons.blue("=== do"),
- crayons.cyan(args.sub_cmd),
- crayons.blue("on repo"),
- crayons.yellow(name),
- crayons.blue("==="),
- )
-
- with AdUtil.chdir(dirn):
- subcmd = "{} {}".format(args.sub_cmd, " ".join(extra_args)).strip()
- code, ret = git_helper.command(subcmd)
- if code != 0:
- print(crayons.red(f"Failed to run command <{subcmd}>\n{ret}"))
- else:
- print(crayons.blue(ret))
-
- # If fact, you can git push --delete origin <tagname>/<branchname>
- #
- # TODO(anjingyu): Load the PRIVIATE-TOKEN from configuration file
- def __special_git_cmd(self, git_helper, args, extra_args):
- print(
- crayons.blue(
- f"Processing repositories in workspace:",
- crayons.yellow(self.ws_helper.current_ws_name),
- )
- )
-
- ws = self.ws_helper.current_ws_dir
-
- sub_cmd = args.sub_cmd
-
- name = args.name
- ref = args.ref
-
- if not ref:
- ref = "master"
- if sub_cmd == "rtag" and not args.delete:
- tags = name.split(".", 2)
-
- if len(tags) == 3:
- ref = "{}.{}.x".format(tags[0], tags[1])
-
- ws = self.ws_helper.current_ws_dir
- for repo_name in os.listdir(ws):
- dirn = os.path.join(ws, repo_name)
-
- if not os.path.isdir(dirn):
- continue
-
- do_delete = args.delete
- with AdUtil.chdir(dirn):
- remote_url = git_helper.url()
-
- msg = ""
- drmsg = ""
- code = 0
- cmsg = ""
-
- if do_delete:
- msg = f"Delete {name} for repository: {repo_name}"
- else:
- msg = f"Create {name} for repository: {repo_name}"
-
- if not args.dry_run:
- if do_delete:
- code, cmsg = git_helper.push(f"origin :{name}")
-
- if sub_cmd == "rtag":
- if do_delete:
- if code == 0:
- code, cmsg = git_helper.tag(_d=name)
- else:
- if git_helper.has_branch(ref):
- code, cmsg = git_helper.tag(f"{name} {ref}")
- if code == 0:
- code, cmsg = git_helper.push(
- f"origin refs/tags/{name}"
- )
- else:
- code = -1
- cmsg = f"{ref} is not existent!"
- else:
- if do_delete:
- if code == 0:
- code, cmsg = git_helper.branch(_D=name)
- code, cmsg = git_helper.branch(f"{name} {ref}")
- if code == 0:
- code, cmsg = git_helper.push(f"origin {name}:{name}")
-
- if code == 0:
- print(crayons.blue(f"{msg: <80}"), crayons.green("[√]"))
- else:
- print(crayons.yellow(f"{msg: <80}"), crayons.red("[×]"))
- print(crayons.red(cmsg))
- else:
- print(crayons.blue(drmsg))
-
-
- def __create_argparser():
- parser = argparse.ArgumentParser(
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="adgit <sub_cmd> <arguments>",
- )
-
- parser.add_argument("-v", "--version", action="version", version=__version__)
- parser.add_argument(
- "--verbose",
- default=False,
- action="store_true",
- help="""Show verbose information.""",
- )
- parser.add_argument(
- "--dry-run",
- default=False,
- action="store_true",
- help="""Only output command, never do operation.""",
- )
-
- parent_parser = [argparse.ArgumentParser(add_help=False)]
-
- subparsers = parser.add_subparsers(dest="sub_cmd", help="Sub-command help")
-
- adgit = AdGit()
-
- for c in adgit.commands:
- sub_parser = subparsers.add_parser(c[0], help=c[1], parents=parent_parser)
- sub_parser.set_defaults(func=c[2])
-
- if c[0] in ["rtag", "rbranch"]:
- sub_parser.add_argument("name", help="{} name.".format(c[0].capitalize()))
- sub_parser.add_argument(
- "-r",
- "--ref",
- default="",
- help="The reference branch, by default, branch is referred to master, and tag is referred to the corresponding branch.",
- )
- sub_parser.add_argument(
- "-d", "--delete", default=False, action="store_true", help="Do delete"
- )
-
- return parser
-
-
- def main():
- with AdUtil.time_consumed():
- parser = __create_argparser()
-
- args = sys.argv[1:]
- if len(args) == 0:
- parser.print_help()
- return 0
-
- args, _extra_args = parser.parse_known_args()
- sub_cmd = args.sub_cmd
-
- git_helper = AdGitHelper(dry_run=args.dry_run)
-
- if args.verbose:
- Logger.get_instance().enable()
-
- # Always replace --help with -h for git sub-command
- if sub_cmd:
- if "--help" in _extra_args or "-h" in _extra_args:
- _, ret = git_helper.command(sub_cmd + " -h")
- # Ignore the error code of help
- D(crayons.blue(ret))
- return 0
-
- args.func(git_helper, args, _extra_args)
-
-
- if __name__ == "__main__":
- main()
|