'''
.. |param_local| replace:: The local folder of the repository
.. |param_remote_url| replace:: The remote URL of the repository
'''
from photon import IDENT
from photon.photon import check_m
from photon.util.locations import search_location
from photon.util.system import get_hostname
[docs]class Git(object):
'''
The git tool helps to deal with git repositories.
:param local:
|param_local|
* If ``None`` given (default), it will be ignored if there \
is already a git repo at `local`
* If no git repo is found at `local`, a new one gets \
cloned from `remote_url`
:param remote_url:
|param_remote_url|
* |appteardown| if `remote_url` is set to ``None`` but \
a new clone is necessary
:param mbranch:
The repository's main branch.
Is set to `master` when left to ``None``
'''
def __init__(self, m, local, remote_url=None, mbranch=None):
super().__init__()
self.m = check_m(m)
self.__local = search_location(local, create_in=local)
self.__remote_url = remote_url
if not mbranch:
mbranch = 'master'
self.__mbranch = mbranch
if self.m(
'checking for git repo',
cmdd=dict(cmd='git rev-parse --show-toplevel', cwd=self.local),
critical=False,
verbose=False
).get('out') != self.local:
if not self.remote_url:
self.m(
'a new git clone without remote url is not possible.',
state=True,
more=dict(local=self.local)
)
self.m(
'cloning into repo',
cmdd=dict(
cmd='git clone %s %s' % (self.remote_url, self.local)
)
)
self.m(
'git tool startup done',
more=dict(remote_url=self.remote_url, local=self.local),
verbose=False
)
@property
def local(self):
'''
:returns:
|param_local|
'''
return self.__local
@property
def remote_url(self):
'''
:returns:
|param_remote_url|
'''
return self.__remote_url
@property
def remote(self):
'''
:returns:
Current remote
'''
return self._get_remote().get('out')
@property
def commit(self):
'''
:param tag:
Checks out specified commit.
If set to ``None`` the latest commit will be checked out
:returns:
A list of all commits, descending
'''
commit = self._log(num=-1, format='%H')
if commit.get('returncode') == 0:
return commit.get('stdout')
@commit.setter
def commit(self, commit):
'''
.. seealso:: :attr:`commit`
'''
c = self.commit
if c:
if not commit:
commit = c[0]
if commit in c:
self._checkout(treeish=commit)
@property
def short_commit(self):
'''
:returns:
A list of all commits, descending
.. seealso:: :attr:`commit`
'''
commit = self._log(num=-1, format='%h')
if commit.get('returncode') == 0:
return commit.get('stdout')
@property
def log(self):
'''
:returns:
The last 10 commit entries as dictionary
* 'commit': The commit-ID
* 'message': First line of the commit message
'''
log = self._log(num=10, format='%h::%b').get('stdout')
if log:
return [
dict(commit=c, message=m) for c, m in [
l.split('::') for l in log
]
]
@property
def status(self):
'''
:returns:
Current repository status as dictionary:
* 'clean': ``True`` if there are no changes ``False`` otherwise
* 'untracked': A list of untracked files (if any and not 'clean')
* 'modified': A list of modified files (if any and not 'clean')
* 'deleted': A list of deleted files (if any and not 'clean')
* 'conflicting': A list of conflicting files (if any and not 'clean')
'''
status = self.m(
'getting git status',
cmdd=dict(cmd='git status --porcelain', cwd=self.local),
verbose=False
).get('stdout')
o, m, f, g = list(), list(), list(), list()
if status:
for w in status:
s, t = w[:2], w[3:]
if '?' in s:
o.append(t)
if 'M' in s:
m.append(t)
if 'D' in s:
f.append(t)
if 'U' in s:
g.append(t)
clean = False if o + m + f + g else True
return dict(
untracked=o, modified=m,
deleted=f, conflicting=g, clean=clean)
@property
def branch(self):
'''
:param branch:
Checks out specified branch (tracking if it exists on remote).
If set to ``None``, 'master' will be checked out
:returns:
The current branch
(This could also be 'master (Detatched-Head)' - Be warned)
'''
branch = self._get_branch().get('stdout')
if branch:
return ''.join(
[b for b in branch if '*' in b]
).replace('*', '').strip()
@branch.setter
def branch(self, branch):
'''
.. seealso:: :attr:`branch`
'''
if not branch:
branch = self.__mbranch
tracking = (
''
if branch in self._get_branch(remotes=True).get('out') else
'-B'
)
self._checkout(treeish='%s %s' % (tracking, branch))
@property
def tag(self):
'''
:param tag:
Checks out specified tag. If set to ``None`` the latest
tag will be checked out
:returns:
A list of all tags, sorted as version numbers, ascending
'''
tag = self.m(
'getting git tags',
cmdd=dict(
cmd='git tag -l --sort="version:refname"',
cwd=self.local
),
verbose=False,
)
if tag.get('returncode') == 0:
return tag.get('stdout')
@tag.setter
def tag(self, tag):
'''
.. seealso:: :attr:`tag`
'''
t = self.tag
if t:
if not tag:
tag = t[-1]
if tag in t:
self._checkout(treeish=tag)
@property
def cleanup(self):
'''
Commits all local changes (if any) into a working branch,
merges it with 'master'.
Checks out your old branch afterwards.
|appteardown| if conflicts are discovered
'''
hostname = get_hostname()
old_branch = self.branch
changes = self.status
if not changes.get('clean'):
self.branch = hostname
utmo = changes.get('untracked', []) + changes.get('modified', [])
for f in utmo:
self.m(
'adding file to repository',
cmdd=(dict(cmd='git add %s' % (f), cwd=self.local)),
more=f,
critical=False
)
for f in changes.get('deleted', []):
self.m(
'deleting file from repository',
cmdd=(dict(cmd='git rm %s' % (f), cwd=self.local)),
more=f,
critical=False
)
if changes.get('conflicting'):
self.m(
'Well done! You have conflicting files in the repository!',
state=True,
more=changes
)
self.m(
'auto commiting changes',
cmdd=dict(
cmd='git commit -m "%s %s auto commit"' % (
hostname, IDENT
),
cwd=self.local
),
more=changes
)
self.branch = None
self.m(
'auto merging branches',
cmdd=dict(
cmd='git merge %s -m "%s %s auto merge"' % (
hostname, hostname, IDENT
),
cwd=self.local
),
more=dict(
branch=old_branch,
temp_branch=hostname
)
)
self.branch = old_branch
return dict(changes=changes, pull=self._pull())
@property
def publish(self):
'''
Runs :func:`cleanup` first,
then pushes the changes to the :attr:`remote`.
'''
self.cleanup
remote = self.remote
branch = self.branch
return self.m(
'pushing changes to %s/%s' % (remote, branch),
cmdd=dict(
cmd='git push -u %s %s' % (remote, branch),
cwd=self.local
),
more=dict(remote=remote, branch=branch)
)
[docs] def _get_remote(self, cached=True):
'''
Helper function to determine remote
:param cached:
Use cached values or query remotes
'''
return self.m(
'getting current remote',
cmdd=dict(
cmd='git remote show %s' % ('-n' if cached else ''),
cwd=self.local
),
verbose=False
)
[docs] def _log(self, num=None, format=None):
'''
Helper function to receive git log
:param num:
Number of entries
:param format:
Use formatted output with specified format string
'''
num = '-n %s' % (num) if num else ''
format = '--format="%s"' % (format) if format else ''
return self.m(
'getting git log',
cmdd=dict(cmd='git log %s %s' % (num, format), cwd=self.local),
verbose=False
)
[docs] def _get_branch(self, remotes=False):
'''
Helper function to determine current branch
:param remotes:
List the remote-tracking branches
'''
return self.m(
'getting git branch information',
cmdd=dict(
cmd='git branch %s' % ('-r' if remotes else ''),
cwd=self.local
),
verbose=False
)
[docs] def _checkout(self, treeish):
'''
Helper function to checkout something
:param treeish:
String for '`tag`', '`branch`', or remote tracking '-B `banch`'
'''
return self.m(
'checking out "%s"' % (treeish),
cmdd=dict(cmd='git checkout %s' % (treeish), cwd=self.local),
verbose=False
)
[docs] def _pull(self):
'''
Helper function to pull from remote
'''
pull = self.m(
'pulling remote changes',
cmdd=dict(cmd='git pull --tags', cwd=self.local),
critical=False
)
if 'CONFLICT' in pull.get('out'):
self.m(
'Congratulations! You have merge conflicts in the repository!',
state=True,
more=pull
)
return pull