#!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK
from __future__ import annotations
from typing import Any
import os
import ubelt as ub
import scriptconfig as scfg
[docs]
class GitRebaseAddContinue(scfg.DataConfig):
"""
A single step to make rebasing easier.
Usually a rebase has the user explicitly add and then continue.
This script checks all of the paths for conflicts and then if none
exist adds all files and continues.
"""
__command__: str = 'rebase_add_continue'
repo_dpath: scfg.Value = scfg.Value('.', help='location of the repo')
skip_editor: scfg.Value = scfg.Value(
True,
help='if True skip the editor to change the commit message on git rebase --continue',
)
[docs]
@classmethod
def main(
cls, argv: list[str] | str | bool | None = True, **kwargs: Any
) -> None:
"""
Example:
>>> from git_well.git_rebase_add_continue import GitRebaseAddContinue
>>> from git_well.repo import Repo
>>> cls = GitRebaseAddContinue
>>> repo = Repo.demo()
>>> # TODO: make a plausible scenario
>>> argv = False
>>> kwargs = dict()
>>> kwargs['repo_dpath'] = repo
>>> import pytest
>>> with pytest.raises(RuntimeError):
>>> cls.main(argv=argv, **kwargs)
"""
config = cls.cli(argv=argv, data=kwargs)
from git_well._utils import rich_print
from git_well.repo import Repo
rich_print('config = {}'.format(ub.urepr(config, nl=1)))
repo = Repo.coerce(config.repo_dpath)
repo_dpath = repo.dpath
fpaths = parsed_rebase_git_status(repo_dpath)
num_paths = ub.udict(fpaths).map_values(len)
print('num_paths = {}'.format(ub.urepr(num_paths, nl=1)))
if 0:
import xdev
b = xdev.RegexBuilder.coerce('python')
b.previous(exact='7')
import re
# Check if conflicts are resolved
conflict_patterns = [
re.compile('^' + ('<' * 7) + ' HEAD$', flags=re.MULTILINE),
re.compile('^' + ('>' * 7) + ' HEAD$', flags=re.MULTILINE),
# re.compile('^' + ('=' * 7) + '$', flags=re.MULTILINE),
re.compile(
'^' + ('>' * 7) + r' [0-9a-f]{7} \(.*\)$', flags=re.MULTILINE
),
re.compile(
'^' + ('<' * 7) + r' [0-9a-f]{7} \(.*\)$', flags=re.MULTILINE
),
]
conflicts = []
for fpath in ub.flatten(fpaths.values()):
try:
text = fpath.read_text()
except IsADirectoryError:
# probably in a submodule
continue
except FileNotFoundError:
text = ''
for pat in conflict_patterns:
if pat.search(text):
conflicts.append(fpath)
break
if conflicts:
print('conflicts = {}'.format(ub.urepr(conflicts, nl=1)))
raise Exception('Paths still have unresolved conflicts')
print('Did not detect any unresolved conflicts')
# If everything looks ok run git add && git rebase --continue
print('Running git add')
repo.git.add(*fpaths['both_modified'], *fpaths['unstaged'])
print('Running git rebase --continue')
if config.skip_editor:
# Use -c core.editor=true to skip the commit message editor
# References:
# ... [SO43489971] https://stackoverflow.com/questions/43489971/how-to-suppress-the-editor-for-git-rebase-continue
info = ub.cmd(
'git -c core.editor=true rebase --continue',
verbose=3,
cwd=repo.dpath,
system=True,
)
else:
info = ub.cmd(
'git rebase --continue', verbose=3, cwd=repo.dpath, system=True
)
if info['ret'] == 0:
print('rebase is complete')
else:
info = ub.cmd('git status', verbose=3, cwd=repo.dpath, system=True)
print('rebase is still active')
[docs]
def parsed_rebase_git_status(
repo_dpath: str | os.PathLike[str],
) -> dict[str, list[ub.Path]]:
"""
a git status output has several possible sections it can output,
check for those, and set the state based on them.
Information within each state will be indented
"""
info = ub.cmd('git status', verbose=3, cwd=repo_dpath)
status = info['out']
# print(status)
# Parse git status to determine paths that have conflicts and need a
# git add.
fpaths = {
'modified': [],
'both_modified': [],
'unstaged': [],
}
print('parse git status')
lines = status.split('\n')
if 'interactive rebase in progress' not in lines[0]:
raise RuntimeError('Not currently rebasing')
line_iter = iter(lines)
state = None
for line_idx, line in enumerate(line_iter):
line_ = line.strip()
if line_ == '':
state = None
# Check for a state change.
elif line.startswith('Last commands done'):
state = 'LAST_COMMANDS'
elif line.startswith('Changes not staged for commit:'):
state = 'UNSTAGED'
assert 'git add' in next(line_iter)
assert 'git restore' in next(line_iter)
elif line.startswith('Changes to be committed:'):
state = 'MODIFIED'
assert 'git restore --staged' in next(line_iter)
elif line.startswith('Unmerged paths:'):
state = 'UNMERGED'
assert 'git restore' in next(line_iter)
assert 'git add <file>' in next(line_iter)
else:
# Parse information with in a state
if state is None:
...
elif state == 'LAST_COMMANDS':
...
elif state == 'MODIFIED':
if line_.startswith(('modified:', 'new file:')):
rel_fpath = line.split(':', 1)[1].strip()
fpath = repo_dpath / rel_fpath
fpaths['modified'].append(fpath)
else:
raise Exception(
ub.paragraph(
f"""
rebase status parser hit unhandled case in state {state}:
line_idx={line_idx}, line={line}
"""
)
)
elif state == 'UNMERGED':
if line_.startswith(('both modified:',)):
rel_fpath = line.split(':', 1)[1].strip()
fpath = repo_dpath / rel_fpath
fpaths['both_modified'].append(fpath)
else:
raise Exception(
ub.paragraph(
f"""
rebase status parser hit unhandled case in state {state}:
line_idx={line_idx}, line={line}
"""
)
)
elif state == 'UNSTAGED':
if line_.startswith('('):
continue
elif line_.startswith(('modified:', 'new file:')):
rel_fpath = line.split(':', 1)[1]
# Hacks for submodules
rel_fpath = rel_fpath.replace(
'(modified content, untracked content)', ''
)
rel_fpath = rel_fpath.strip()
fpath = repo_dpath / rel_fpath
fpaths['unstaged'].append(fpath)
else:
raise Exception(
ub.paragraph(
f"""
rebase status parser hit unhandled case in state {state}:
line_idx={line_idx}, line={line}
"""
)
)
else:
raise Exception(
ub.paragraph(
f"""
rebase status parser hit unhandled case in state {state}:
line_idx={line_idx}, line={line}
"""
)
)
print('finished parse git status')
return fpaths
__cli__ = GitRebaseAddContinue
main = __cli__.main
if __name__ == '__main__':
"""
CommandLine:
python ~/code/git_well/git_well/git_rebase_add_continue.py
"""
__cli__.main()