Source code for utool.util_git

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
TODO: export from utool

        python -m utool.util_inspect check_module_usage --pat="util_git.py"
"""
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
import sys
import os
import re
import six
from six.moves import zip
from os.path import exists, join, dirname, isdir, basename
from utool import util_dev
from utool import util_inject
from utool import util_class
from utool import util_path
from utool import util_decor
from utool import util_list

print, rrr, profile = util_inject.inject2(__name__)


def _syscmd(cmdstr):
    print('RUN> ' + cmdstr)
    os.system(cmdstr)


def _cd(dir_):
    dir_ = util_path.truepath(dir_)
    print('> cd ' + dir_)
    os.chdir(dir_)


[docs]@six.add_metaclass(util_class.ReloadingMetaclass) class RepoManager(util_dev.NiceRepr): """ Batch git operations on multiple repos """ def __init__( rman, repo_urls=None, code_dir=None, userid=None, permitted_repos=None, label='', pythoncmd=None, ): if userid is None: userid = None if permitted_repos is None: permitted_repos = [] rman.permitted_repos = permitted_repos rman.code_dir = code_dir rman.userid = userid rman.repos = [] rman.label = label rman.pythoncmd = pythoncmd if repo_urls is not None: rman.add_repos(repo_urls)
[docs] def union(rman, other): new = RepoManager() new.userid = rman.userid new.code_dir = rman.code_dir new.permitted_repos = rman.permitted_repos + other.permitted_repos new.repos = rman.repos + other.repos return new
@property def repo_urls(rman): return [repo.url for repo in rman.repos] @property def repo_dirs(rman): return [repo.url for repo in rman.repos] def __nice__(rman): return '(num=%d)' % (len(rman.repo_urls)) def __getitem__(rman, name): for repo in rman.repos: if name in repo.aliases: return repo raise KeyError(name)
[docs] def ensure(rman): print('Ensuring that respos are checked out') for repo in rman.repos: if repo.url is not None: repo.clone()
[docs] def add_repo(rman, repo): repo._fix_url(rman.userid, rman.permitted_repos) rman.repos.append(repo)
[docs] def add_repos(rman, repo_urls=None, code_dir=None): if code_dir is None: code_dir = rman.code_dir assert code_dir is not None, 'Must specify the checkout code_dir' repos = [Repo(url, code_dir, pythoncmd=rman.pythoncmd) for url in repo_urls] for repo in repos: repo._fix_url(rman.userid, rman.permitted_repos) rman.repos.extend(repos)
[docs] def issue(rman, command, sudo=False): """Runs a command on all of managed repos""" print('+------- GG_COMMAND -------') print('| sudo=%s' % sudo) print('| command=%s' % command) for repo in rman.repos: if exists(repo.dpath): repo.issue(command, sudo=sudo) else: print('Repo %r not found' % (repo,)) print('L___ FINISHED GG_COMMAND ___')
[docs] def check_importable(rman): import utool as ut label = ' %s' % rman.label if rman.label else rman.label missing = [] print('Checking if%s modules are importable' % (label,)) msg_list = [] recommended_fixes = [] for repo in rman.repos: flag, msg, errors = repo.check_importable() if not flag: msg_list.append( ' * !!!%s REPO %s HAS IMPORT ISSUES' % (label.upper(), repo) ) if any([str(ex).find('undefined symbol') > -1 for ex in errors]): recommended_fixes.append('rebuild') else: recommended_fixes.append(None) if ut.VERBOSE: msg_list.append(ut.indent(msg, ' ')) missing.append(repo) else: if ut.VERBOSE: msg_list.append(ut.indent(msg, ' ')) print('\n'.join(msg_list)) problems = list(zip(missing, recommended_fixes)) return problems
[docs] def check_installed(rman): import utool as ut label = ' %s' % rman.label if rman.label else rman.label missing = [] msg_list = [] print('Checking if%s modules are installed' % (label,)) for repo in rman.repos: flag, msg = repo.check_installed() if not flag: msg_list.append( ' * !!!%s REPO %s NEEDS TO BE INSTALLED' % (label.upper(), repo) ) if ut.VERBOSE: msg_list.append(ut.indent(msg, ' ')) missing.append(repo) # else: # print(' * found%s module = %s' % (label, repo,)) print('\n'.join(msg_list)) return missing
[docs] def check_cpp_build(rman): import utool as ut label = ' %s' % rman.label if rman.label else rman.label missing = [] print('Checking if%s modules are built' % (label,)) for repo in rman.repos: flag, msg = repo.check_cpp_build() if not flag: print(' * !!!%s REPO %s NEEDS TO BE BUILT' % (label.upper(), repo)) if ut.VERBOSE: print(ut.indent(msg, ' ')) missing.append(repo) return missing
[docs] def custom_build(rman): print('Custom Build') for repo in rman.repos: script = repo.get_script('build') if script is not None: script.exec_()
[docs] def custom_install(rman): print('Custom Install') for repo in rman.repos: script = repo.get_script('install') if script is not None: script.exec_()
[docs] def only_with_pysetup(rman): rman2 = RepoManager() rman2.code_dir = rman.code_dir rman2.permitted_repos = rman.permitted_repos rman2.code_dir = rman.code_dir rman2.userid = rman.userid rman2.label = rman.label rman2.repos = [ repo for repo in rman.repos if repo.dpath and exists(join(repo.dpath, 'setup.py')) ] return rman2
[docs]@six.add_metaclass(util_class.ReloadingMetaclass) class Repo(util_dev.NiceRepr): """ Handles a Python module repository """ def __init__(repo, url=None, code_dir=None, dpath=None, modname=None, pythoncmd=None): # modname might need to be called egg? import utool as ut if url is not None and '.git@' in url: # parse out specific branch repo.default_branch = url.split('@')[-1] url = '@'.join(url.split('@')[:-1]) else: repo.default_branch = None repo.url = url repo._modname = None if modname is None: modname = [] repo._modname_hints = ut.ensure_iterable(modname) repo.dpath = None repo.scripts = {} if pythoncmd is None: import sys pythoncmd = sys.executable repo.pythoncmd = pythoncmd if dpath is None and repo.url is not None and code_dir is not None: dpath = join(code_dir, repo.reponame) if dpath is not None: repo.dpath = util_path.unixpath(dpath)
[docs] def infer_info(repo): if repo.url is None: repo.url = list(repo.as_gitpython().remotes[0].urls)[0]
# --- GIT PYTHON STUFF ---
[docs] @util_decor.memoize def as_gitpython(repo): """pip install gitpython""" import git gitrepo = git.Repo(repo.dpath) return gitrepo
@property def remotes(repo): remotes = repo.as_gitpython().remotes remote_dict = {} for remote in remotes: pass remote_info = repo._remote_info(remote) if remote_info is not None: name = remote_info.pop('name') remote_dict[name] = remote_info return remote_dict def _remote_info(repo, remote): OLD = False if OLD: remote_details = remote.repo.git.remote('get-url', remote.name, '--push') # TODO push into gitpython urls = [line for line in remote_details.split('\n')] else: urls = list(remote.urls) # urls = list(remote.urls) if len(urls) == 0: print('[git] WARNING: repo %r has no remote urls' % (repo,)) remote_info = None else: if len(urls) > 1: print('[git] WARNING: repo %r has multiple urls' % (repo,)) url = urls[0] url = url.replace('github.com:/', 'github.com:') remote_info = {} url_parts = re.split('[@/:]', url) # TODO: parse what format the url is in, ssh/http/https idx = util_list.listfind(url_parts, 'github.com') remote_info['name'] = remote.name remote_info['url'] = url if idx is not None: username = url_parts[idx + 1] remote_info['host'] = 'github' remote_info['username'] = username return remote_info def _ensure_remote_exists(repo, remote_name, remote_url, fmt=None): # Remote the remote if it is not in the correct format if fmt is None: if remote_url.startswith('git@'): fmt = 'ssh' elif remote_url.startswith('https://'): fmt = 'https' else: raise ValueError('bad format') gitrepo = repo.as_gitpython() remotes = repo.remotes incorrect_version = False if remote_url in remotes: # Check correct version (SSH or HTTPS) wildme_remote_ = remotes[remote_url] wildme_url_ = wildme_remote_['url'] is_ssh = '@' in wildme_url_ incorrect_version = (is_ssh and fmt == 'https') or ( not is_ssh and fmt == 'ssh' ) if incorrect_version: print( ' * Deleting bad version remote %r: %r' % (remote_name, remote_url) ) gitrepo.delete_remote(remote_name) # Ensure there is a remote under the wildme name if remote_name not in repo.remotes or incorrect_version: print(' * Create remote %r: %r' % (remote_name, remote_url)) gitrepo.create_remote(remote_name, remote_url) return incorrect_version def _new_remote_url(repo, host=None, user=None, reponame=None, fmt=None): import utool as ut if reponame is None: reponame = repo.reponame if host is None: host = 'github.com' if fmt is None: fmt = 'ssh' if host == 'github.com': assert user is not None, 'github needs a user' url_fmts = { 'https': ('https://', '/'), 'ssh': ('git@', ':'), } prefix, sep = url_fmts[fmt] user_ = '' if user is None else user + '/' parts = [prefix, host, sep, user_, reponame, '.git'] parts = ut.filter_Nones(parts) url = ''.join(parts) return url
[docs] def reset_branch_to_remote(repo, branch, hard=True): """ does a git reset --hard to whatever remote the branch is assigned to """ remote = repo.get_branch_remote(branch) kw = dict(remote=remote, branch=branch) if hard: kw['flags'] = '--hard' repo.issue('git reset {flags} {remote}/{branch}'.format(**kw))
[docs] def get_branch_remote(repo, branch): gitrepo = repo.as_gitpython() gitbranch = gitrepo.branches[branch] remote = gitbranch.tracking_branch().remote_name return remote
[docs] def set_branch_remote(repo, branch, remote, remote_branch=None): if remote_branch is None: remote_branch = branch fmt = 'git branch --set-upstream-to={remote}/{remote_branch} {branch} ' cmd = fmt.format(branch=branch, remote=remote, remote_branch=remote_branch) repo.issue(cmd)
@property def active_branch(repo): return repo.as_gitpython().active_branch.name @property def active_remote(repo): branch = repo.as_gitpython().active_branch tracking_branch = branch.tracking_branch() remote_info = None for remote in repo.as_gitpython().remotes: pass if remote.name == tracking_branch.remote_name: remote_info = repo._remote_info(remote) break return remote_info @property def active_tracking_remote_head(repo): branch = repo.as_gitpython().active_branch tracking_branch = branch.tracking_branch() return tracking_branch.remote_head @property def active_tracking_branch_name(repo): branch = repo.as_gitpython().active_branch tracking_branch = branch.tracking_branch() return tracking_branch.name @property def branches(repo): gitrepo = repo.as_gitpython() return [branch.name for branch in gitrepo.branches] # --- </GIT PYTHON STUFF> --- @property def aliases(repo): aliases = [] if repo._modname is not None: aliases.append(repo._modname) aliases.extend(repo._modname_hints[:]) # if repo.dpath and exists(repo.dpath): # reponame = repo._find_modname_from_repo() # if reponame is not None: # aliases.append(reponame) aliases.append(repo.reponame) aliases.append(repo.reponame.lower()) import utool as ut aliases = ut.unique(aliases) return aliases def __nice__(repo): reponame = repo.reponame modname = repo.modname # if modname is False: # print(repo.__dict__) if modname is None or modname == reponame: return '(%s)' % (reponame,) else: return '(%s, %s)' % (reponame, modname) @property def modname(repo): # import utool as ut modname = None if repo._modname is not None: modname = repo._modname elif len(repo._modname_hints) == 1: modname = repo._modname_hints[0] else: modname = repo.aliases[0] return modname @property def reponame(repo): if repo.dpath is not None: reponame = basename(repo.dpath) elif repo.url is not None: url_parts = re.split('[/:]', repo.url) reponame = url_parts[-1].replace('.git', '') elif repo._modname_hints: reponame = repo._modname_hints[0] else: raise Exception('No way to infer (or even guess) repository name!') return reponame def _find_modname_from_repo(repo): import utool as ut packages = ut.get_submodules_from_dpath( repo.dpath, only_packages=True, recursive=False ) if len(packages) == 1: modname = ut.get_modname_from_modpath(packages[0]) return modname
[docs] def add_script(repo, key, script): repo.scripts[key] = script
[docs] def clone(repo, recursive=False): print('[git] check repo exists at %s' % (repo.dpath)) if recursive: args = '--recursive' else: args = '' if not exists(repo.dpath): _cd(dirname(repo.dpath)) print('repo.default_branch = %r' % (repo.default_branch,)) if repo.default_branch is not None: args += ' -b {}'.format(repo.default_branch) _syscmd('git clone {args} {url}'.format(args=args, url=repo.url))
[docs] def owner(repo): url_parts = re.split('[/:]', repo.url) owner = url_parts[-2] return owner
[docs] def is_owner(repo, userid): return userid is not None and repo.owner.lower() == userid.lower()
def _fix_url(repo, userid, permitted_repos=[]): is_owner = repo.is_owner(userid) is_contrib = repo.reponame in permitted_repos if is_owner or is_contrib: repo._ensure_ssh_format()
[docs] def change_url_format(repo, out_type='ssh'): """Changes the url format for committing""" url = repo.url url_parts = re.split('[/:]', url) in_type = url_parts[0] url_fmts = { 'https': ('.com/', 'https://'), 'ssh': ('.com:', 'git@'), } url_fmts['git'] = url_fmts['ssh'] new_repo_url = url for old, new in zip(url_fmts[in_type], url_fmts[out_type]): new_repo_url = new_repo_url.replace(old, new) # Inplace change repo.url = new_repo_url print('new format repo.url = {!r}'.format(repo.url))
[docs] def check_importable(repo): import utool as ut # import utool as ut found = False tried = [] errors = [] for modname in repo.aliases: tried.append(modname) try: ut.import_modname(modname) except ImportError as ex: # NOQA tried[-1] += ' but got ImportError' errors.append(ex) pass except AttributeError as ex: # NOQA tried[-1] += ' but got AttributeError' errors.append(ex) else: found = True errors.append(None) tried[-1] += ' and it worked' break msg = 'tried %s' % (', '.join(tried)) return found, msg, errors
[docs] def is_cloned(repo): from os.path import exists if not exists(repo.dpath): return False
[docs] def check_installed(repo): import utool as ut found = None tried = [] for modname in repo.aliases: tried.append(modname) found = ut.check_module_installed(modname) if found: break msg = 'tried %s' % (', '.join(tried)) return found, msg
[docs] def check_cpp_build(repo): import utool as ut script = repo.get_script('build') if script.is_fpath_valid(): if repo.modname == 'pyflann': return True, 'cant detect flann cpp' # hack, this doesnt quite do it pat = '*' + ut.util_cplat.get_pylib_ext() dynlibs = ut.glob(repo.dpath + '/' + repo.modname, pat, recursive=True) msg = 'Could not find any dynamic libraries' flag = len(dynlibs) > 0 else: flag = True msg = 'passed, but didnt expect anything' return flag, msg
[docs] def get_script(repo, type_): class Script(object): def __init__(script): script.type_ = type_ script.text = None script.fpath = None script.cmake = None def is_fpath_valid(script): return script.fpath is not None and exists(script.fpath) def is_valid(script): return script.text or script.is_fpath_valid() def exec_(script): import utool as ut print('+**** exec %s script *******' % (script.type_)) print('repo = %r' % (repo,)) with ut.ChdirContext(repo.dpath): if script.is_fpath_valid(): normbuild_flag = '--no-rmbuild' if ut.get_argflag(normbuild_flag): ut.cmd(script.fpath + ' ' + normbuild_flag) else: ut.cmd(script.fpath) else: if script.text is not None: print('ABOUT TO EXECUTE') ut.print_code(script.text, 'bash') if ut.are_you_sure('execute above script?'): from os.path import join scriptdir = ut.ensure_app_resource_dir( 'utool', 'build_scripts' ) script_path = join( scriptdir, 'script_' + script.type_ + '_' + ut.hashstr27(script.text) + '.sh', ) ut.writeto(script_path, script.text) _ = ut.cmd('bash ', script_path) # NOQA else: print('CANT QUITE EXECUTE THIS YET') ut.print_code(script.text, 'bash') # os.system(scriptname) print('L**** exec %s script *******' % (script.type_)) script = Script() script.text = repo.scripts.get(type_, None) if script.text is None and type_ == 'build' and repo.dpath: if sys.platform.startswith('win32'): # vtool --rebuild-sver didnt work with this line # scriptname = './mingw_build.bat' fpath = join(repo.dpath, 'mingw_build.bat') else: fpath = join(repo.dpath, 'unix_build.sh') if exists(fpath): script.fpath = fpath cmake = join(repo.dpath, 'CMakeLists.txt') if exists(cmake): script.cmake = cmake return script
[docs] def has_script(repo, type_): return repo.get_script(type_).is_valid()
[docs] def custom_build(repo): script = repo.get_script('build') if script is not None: script.exec_()
[docs] def custom_install(repo): script = repo.get_script('install') if script is not None: script.exec_()
# TODO: # import utool as ut # ut.print_code(repo.install_script, 'bash')
[docs] def issue(repo, command, sudo=False, dry=False, error='raise', return_out=False): """ issues a command on a repo CommandLine: python -m utool.util_git --exec-repocmd Example: >>> # DISABLE_DOCTEST >>> from utool.util_git import * # NOQA >>> import utool as ut >>> repo = dirname(ut.get_modpath(ut, prefer_pkg=True)) >>> command = 'git status' >>> sudo = False >>> result = repocmd(repo, command, sudo) >>> print(result) """ import utool as ut if ut.WIN32: assert not sudo, 'cant sudo on windows' if command == 'short_status': return repo.short_status() command_list = ut.ensure_iterable(command) cmdstr = '\n '.join([cmd_ for cmd_ in command_list]) if not dry: print('+--- *** repocmd(%s) *** ' % (cmdstr,)) print('repo=%s' % ut.color_text(repo.dpath, 'yellow')) verbose = True with repo.chdir_context(): ret = None for count, cmd in enumerate(command_list): if dry: print(cmd) continue if not sudo or ut.WIN32: # ret = os.system(cmd) cmdinfo = ut.cmd2(cmd, verbout=True) out, err, ret = ut.take(cmdinfo, ['out', 'err', 'ret']) else: # cmdinfo = ut.cmd2('sudo ' + cmd, verbose=1) out, err, ret = ut.cmd(cmd, sudo=True) if verbose > 1: print('ret(%d) = %r' % (count, ret)) if ret != 0: if error == 'raise': raise Exception('Failed command %r' % (cmd,)) elif error == 'return': return out else: raise ValueError('unknown flag error=%r' % (error,)) if return_out: return out if not dry: print('L____')
[docs] def chdir_context(repo, verbose=False): import utool as ut return ut.ChdirContext(repo.dpath, verbose=verbose)
[docs] def pull2(repo, overwrite=True): """ Pulls and automatically overwrites conflict files. """ cmd = 'git pull --no-edit' out = repo.issue(cmd, error='return') if overwrite and out is not None: repo._handle_overwrite_error(out) # Retry repo.issue(cmd)
[docs] def checkout2(repo, branch, overwrite=True): """ Checkout `branch` and automatically overwrites conflict files. """ cmd = 'git checkout %s' % (branch,) out = repo.issue(cmd, error='return') if overwrite and out is not None: repo._handle_overwrite_error(out) repo._handle_abort_merge_rebase(out) # Retry repo.issue(cmd)
def _parse_merge_conflict_fpaths(repo, out): fpaths = [] if out is not None: for line in out.split('\n'): pref = 'CONFLICT (content): Merge conflict in ' if line.startswith(pref): fpaths.append(join(repo.dpath, line[len(pref) :])) return fpaths def _handle_abort_merge_rebase(repo, out): if out.startswith('error: you need to resolve your current index first'): try: repo.issue('git merge --abort') except Exception: pass try: repo.issue('git rebase --abort') except Exception: pass def _handle_overwrite_error(repo, out): import utool as ut # parse stdout to handle the error if out.startswith( 'error: The following untracked working tree files would be overwritten' ): print('[ut.git] handling overwrite error') lines = out.split('\n')[1:] fpaths = [] for line in lines: if line.startswith('Please move or remove them before you can merge'): break fpaths.append(join(repo.dpath, line.strip())) ut.remove_file_list(fpaths)
[docs] def short_status(repo): r""" CommandLine: python -m utool.util_git short_status Example: >>> # DISABLE_DOCTEST >>> from utool.util_git import * # NOQA >>> import utool as ut >>> repo = Repo(dpath=ut.truepath('.')) >>> result = repo.short_status() >>> print(result) """ import utool as ut prefix = repo.dpath with ut.ChdirContext(repo.dpath, verbose=False): out, err, ret = ut.cmd('git status', verbose=False, quiet=True) # parse git status is_clean_msg1 = 'Your branch is up-to-date with' is_clean_msgs = [ 'nothing to commit, working directory clean', 'nothing to commit, working tree clean', ] msg2 = 'nothing added to commit but untracked files present' needs_commit_msgs = [ 'Changes to be committed', 'Changes not staged for commit', 'Your branch is ahead of', ] suffix = '' if is_clean_msg1 in out and any(msg in out for msg in is_clean_msgs): suffix += ut.color_text('is clean', 'blue') if msg2 in out: suffix += ut.color_text('has untracked files', 'yellow') if any(msg in out for msg in needs_commit_msgs): suffix += ut.color_text('has changes', 'red') print(prefix + ' ' + suffix)
[docs] def python_develop(repo): import utool as ut repo.issue( '{pythoncmd} -m pip install -e {dpath}'.format( pythoncmd=repo.pythoncmd, dpath=repo.dpath ), sudo=not ut.in_virtual_env(), )
[docs] def is_gitrepo(repo): gitdir = join(repo.dpath, '.git') return exists(gitdir) and isdir(gitdir)
[docs] def pull(repo, has_submods=False): print('Pulling: ' + repo.dpath) _cd(repo.dpath) assert repo.is_gitrepo(), 'cannot pull a nongit repo' _syscmd('git pull') if has_submods: _syscmd('git submodule init') _syscmd('git submodule update')
[docs] def rename_branch(repo, old_branch_name, new_branch_name, remote='origin'): r""" References: http://stackoverflow.com/questions/1526794/rename?answertab=votes#tab-top http://stackoverflow.com/questions/9524933/renaming-a-branch-in-github CommandLine: python -m utool.util_git --test-rename_branch --old=mymaster --new=wbia_master Example: >>> # DISABLE_DOCTEST >>> # SCRIPT >>> from utool.util_git import * # NOQA >>> repo = ut.get_argval('--repo', str, '.') >>> remote = ut.get_argval('--remote', str, 'origin') >>> old_branch_name = ut.get_argval('--old', str, None) >>> new_branch_name = ut.get_argval('--new', str, None) >>> rename_branch(old_branch_name, new_branch_name, repo, remote) """ assert repo.is_gitrepo(), 'cannot pull a nongit repo' fmtdict = dict( remote=remote, old_branch_name=old_branch_name, new_branch_name=new_branch_name, ) command_list = [ 'git checkout {old_branch_name}'.format(**fmtdict), # rename branch 'git branch -m {old_branch_name} {new_branch_name}'.format(**fmtdict), # delete old branch 'git push {remote} :{old_branch_name}'.format(**fmtdict), # push new branch 'git push {remote} {new_branch_name}'.format(**fmtdict), ] repo.issue(command_list)
[docs] @staticmethod def resolve_conflicts(fpath, strat, force=False, verbose=True): """ Parses merge conflits and takes either version """ import utool as ut import re top_pat = re.escape('<' * 7) mid_pat = re.escape('=' * 7) bot_pat = re.escape('>' * 7) flags = re.MULTILINE | re.DOTALL # Pattern to remove the top part theirs_pat1 = re.compile('^%s.*?%s.*?$\n' % (top_pat, mid_pat), flags=flags) theirs_pat2 = re.compile('^%s.*?$\n' % (bot_pat), flags=flags) # Pattern to remove the bottom part ours_pat1 = re.compile('^%s.*?%s.*?$\n' % (mid_pat, bot_pat), flags=flags) ours_pat2 = re.compile('^%s.*?$\n' % (top_pat), flags=flags) strat_pats = { 'theirs': [theirs_pat1, theirs_pat2], 'ours': [ours_pat1, ours_pat2], } text_in = ut.readfrom(fpath) text_out = text_in strat = 'ours' strat = 'theirs' for pat in strat_pats[strat]: text_out = pat.sub('', text_out) if verbose: ut.print_difftext(ut.difftext(text_in, text_out, num_context_lines=3)) if force: ut.writeto(fpath, text_out)
[docs]def git_sequence_editor_squash(fpath): r""" squashes wip messages CommandLine: python -m utool.util_git --exec-git_sequence_editor_squash Example: >>> # DISABLE_DOCTEST >>> # SCRIPT >>> import utool as ut >>> from utool.util_git import * # NOQA >>> fpath = ut.get_argval('--fpath', str, default=None) >>> git_sequence_editor_squash(fpath) Ignore: >>> text = ut.codeblock(''' >>> pick 852aa05 better doctest for tips >>> pick 3c779b8 wip >>> pick 02bc21d wip >>> pick 1853828 Fixed root tablename >>> pick 9d50233 doctest updates >>> pick 66230a5 wip >>> pick c612e98 wip >>> pick b298598 Fixed tablename error >>> pick 1120a87 wip >>> pick f6c4838 wip >>> pick 7f92575 wip >>> ''') Notes: # http://stackoverflow.com/questions/8226278/git-alias-to-squash-all-commits-with-a-particular-commit-message # Can do interactively with this. Can it be done automatically and pay attention to # Timestamps etc? git rebase --interactive HEAD~40 --autosquash git rebase --interactive $(git merge-base HEAD master) --autosquash # Lookbehind correct version %s/\([a-z]* [a-z0-9]* wip\n\)\@<=pick \([a-z0-9]*\) wip/squash \2 wip/gc # THE FULL NON-INTERACTIVE AUTOSQUASH SCRIPT # TODO: Dont squash if there is a one hour timedelta between commits GIT_EDITOR="cat $1" GIT_SEQUENCE_EDITOR="python -m utool.util_git --exec-git_sequence_editor_squash \ --fpath $1" git rebase -i $(git rev-list HEAD | tail -n 1) --autosquash --no-verify GIT_EDITOR="cat $1" GIT_SEQUENCE_EDITOR="python -m utool.util_git --exec-git_sequence_editor_squash \ --fpath $1" git rebase -i HEAD~10 --autosquash --no-verify GIT_EDITOR="cat $1" GIT_SEQUENCE_EDITOR="python -m utool.util_git --exec-git_sequence_editor_squash \ --fpath $1" git rebase -i $(git merge-base HEAD master) --autosquash --no-verify # 14d778fa30a93f85c61f34d09eddb6d2cafd11e2 # c509a95d4468ebb61097bd9f4d302367424772a3 # b0ffc26011e33378ee30730c5e0ef1994bfe1a90 # GIT_SEQUENCE_EDITOR=<script> git rebase -i <params> # GIT_SEQUENCE_EDITOR="echo 'FOOBAR $1' " git rebase -i HEAD~40 --autosquash # git checkout master # git branch -D tmp # git checkout -b tmp # option to get the tail commit $(git rev-list HEAD | tail -n 1) # GIT_SEQUENCE_EDITOR="python -m utool.util_git --exec-git_sequence_editor_squash \ # --fpath $1" git rebase -i HEAD~40 --autosquash # GIT_SEQUENCE_EDITOR="python -m utool.util_git --exec-git_sequence_editor_squash \ # --fpath $1" git rebase -i HEAD~40 --autosquash --no-verify <params> """ # print(sys.argv) import utool as ut text = ut.read_from(fpath) # print('fpath = %r' % (fpath,)) print(text) # Doesnt work because of fixed witdth requirement # search = (ut.util_regex.positive_lookbehind('[a-z]* [a-z0-9]* wip\n') + 'pick ' + # ut.reponamed_field('hash', '[a-z0-9]*') + ' wip') # repl = ('squash ' + ut.bref_field('hash') + ' wip') # import re # new_text = re.sub(search, repl, text, flags=re.MULTILINE) # print(new_text) prev_msg = None prev_dt = None new_lines = [] def get_commit_date(hashid): out, err, ret = ut.cmd( 'git show -s --format=%ci ' + hashid, verbose=False, quiet=True, pad_stdout=False, ) # from datetime import datetime from dateutil import parser # print('out = %r' % (out,)) stamp = out.strip('\n') # print('stamp = %r' % (stamp,)) dt = parser.parse(stamp) # dt = datetime.strptime(stamp, '%Y-%m-%d %H:%M:%S %Z') # print('dt = %r' % (dt,)) return dt for line in text.split('\n'): commit_line = line.split(' ') if len(commit_line) < 3: prev_msg = None prev_dt = None new_lines += [line] continue action = commit_line[0] hashid = commit_line[1] msg = ' '.join(commit_line[2:]) try: dt = get_commit_date(hashid) except ValueError: prev_msg = None prev_dt = None new_lines += [line] continue orig_msg = msg can_squash = action == 'pick' and msg == 'wip' and prev_msg == 'wip' if prev_dt is not None and prev_msg == 'wip': tdelta = dt - prev_dt # Only squash closely consecutive commits threshold_minutes = 45 td_min = tdelta.total_seconds() / 60.0 # print(tdelta) can_squash &= td_min < threshold_minutes msg = msg + ' -- tdelta=%r' % (ut.get_timedelta_str(tdelta),) if can_squash: new_line = ' '.join(['squash', hashid, msg]) new_lines += [new_line] else: new_lines += [line] prev_msg = orig_msg prev_dt = dt new_text = '\n'.join(new_lines) def get_commit_date(hashid): out = ut.cmd('git show -s --format=%ci ' + hashid, verbose=False) print('out = %r' % (out,)) # print('Dry run') # ut.dump_autogen_code(fpath, new_text) print(new_text) ut.write_to(fpath, new_text, n=None)
[docs]def std_build_command(repo='.'): """ DEPRICATE My standard build script names. Calls mingw_build.bat on windows and unix_build.sh on unix """ import utool as ut print('+**** stdbuild *******') print('repo = %r' % (repo,)) if sys.platform.startswith('win32'): # vtool --rebuild-sver didnt work with this line # scriptname = './mingw_build.bat' scriptname = 'mingw_build.bat' else: scriptname = './unix_build.sh' if repo == '': # default to cwd repo = '.' else: os.chdir(repo) ut.assert_exists(scriptname) normbuild_flag = '--no-rmbuild' if ut.get_argflag(normbuild_flag): scriptname += ' ' + normbuild_flag # Execute build ut.cmd(scriptname) # os.system(scriptname) print('L**** stdbuild *******')
if __name__ == '__main__': r""" CommandLine: python -m utool.util_git python -m utool.util_git --allexamples """ import multiprocessing multiprocessing.freeze_support() # for win32 import utool as ut # NOQA ut.doctest_funcs()