# labtools, Copyright (C) 2017 Jerry Fowler and Paul Scheet.
# This program comes with ABSOLUTELY NO WARRANTY. It is licensed under
# GNU GPL Version 3. License and warranty may be viewed in the manual.
'''
Wrap some git commands for use in programmatic revision control.
Originally developed for samplemap.py, so it has limited functionality
dictated by that program's limited use cases.
Generalized because I know I would like to use it eventually in syqada.
'''
import os
import sys
import gzip
import re
import socket
import subprocess
from labtools import const
from labtools import misc
from labtools.labexceptions import LabtoolsWarning
CLEAN = 'clean'
UNTRACKED = 'untracked'
MODIFIED = 'modified'
ADDED = 'added'
INVALID = 'invalid'
VERSION_CMD = ['git', '--version']
STATUS_CMD = ['git', 'status']
ADD_CMD = ['git', 'add']
LOG_CMD = ['git', 'log', '-1']
RESET_CMD = ['git', 'reset', '--hard']
COMMIT_CMD = ['git', 'commit', '-m']
COMMITALL_CMD = ['git', 'commit', '-a', '-m']
INIT_CMD = ['git', 'init', '--shared=group']
GIT_BRANCH = 'master'
GIT_HEADER = 'On branch %s' % (GIT_BRANCH)
INIT_STATUS = 'nothing to commit (create/copy files and use "git add" to track)'
CLEAN_STATUS = 'nothing to commit, working directory clean'
UNTRACKED_STATUS = 'Untracked files:'
MODIFIED_STATUS = 'Changes not staged for commit:'
ADDED_STATUS = 'Changes to be committed:'
GIT_OBJECT_DIRECTORY = 'GIT_OBJECT_DIRECTORY'
GIT_DIR = 'GIT_DIR'
[docs]def clean_environment(terms=[GIT_OBJECT_DIRECTORY, GIT_DIR]):
'''
Make sure that the user running this doesn't have these two
environment variables set (a different list may
be expunged by providing a list of terms upon invocation).
'''
for term in terms:
if term in os.environ:
os.environ.pop(term)
[docs]def git_version_ok(version=None):
'''
Make sure the version of git supports our needs. Default 2.3.8.
*version* is only for testing, or possibly for fixing failures caused
by this method refusing to accept values that actually work with this module.
'''
cmd = VERSION_CMD
version = version if version else 'git version 2.3.8'
try:
result = subprocess.check_output(cmd).decode()
except subprocess.CalledProcessError as e:
print(e, file=sys.stderr)
return False
return version <= result
[docs]def git_filestatus(file, gitdir):
'''
Test the status of a file.
Return one of CLEAN, UNTRACKED, MODIFIED, ADDED, INVALID
Statuses of INVALID print error messages to sys.stderr
if *file* is None, return the whole result of git status
*gitdir* is the path to the repository
'''
if not file:
cmd = STATUS_CMD
else:
cmd = STATUS_CMD + [file]
if not os.path.exists(os.path.join(gitdir, file)):
print('No such file', file, file=sys.stderr)
return INVALID
try:
result = subprocess.check_output(cmd, cwd=gitdir).decode()
except subprocess.CalledProcessError as e:
print(e, file=sys.stderr)
return INVALID
if not result.startswith(GIT_HEADER):
print("I expected to be on branch '%s'" % (GIT_BRANCH), file=sys.stderr)
return INVALID
if not file:
return result
if (misc.one_is_true([line.startswith(CLEAN_STATUS) for line in result.split(const.NEWLINE)]) or
misc.one_is_true([line.startswith(INIT_STATUS) for line in result.split(const.NEWLINE)])):
return CLEAN
if file not in result:
print('No file', file, 'found', file=sys.stderr)
return INVALID
if misc.one_is_true([line.startswith(UNTRACKED_STATUS) for line in result.split(const.NEWLINE)]):
return UNTRACKED
if misc.one_is_true([line.startswith(MODIFIED_STATUS) for line in result.split(const.NEWLINE)]):
return MODIFIED
if misc.one_is_true([line.startswith(ADDED_STATUS) for line in result.split(const.NEWLINE)]):
return ADDED
print('Unexpected status result:', result, file=sys.stderr)
return INVALID
[docs]def git_fileadd(file, gitdir):
'''
git add a file for later commit.
Return True upon success, False upon failure.
*gitdir* is the path to the repository
'''
try:
status = git_filestatus(file, gitdir)
except subprocess.CalledProcessError as e:
print(e, file=sys.stderr)
return False
if status not in [UNTRACKED, ADDED, MODIFIED]:
#print("fileadd %s '%s'" % (file, status))
return False
rc = subprocess.call(ADD_CMD + [file], cwd=gitdir)
#print('fileadd status', rc)
return rc == 0
[docs]def git_filecommit(file, message, gitdir, already_controlled=True):
'''
Add and commit a single file, using *message* as the commit message.
Return True upon success, False upon failure.
if *already_controlled* is true, fail if the file is not already under
version control in the repository. Default True on the assumption that
this library is mostly about tight control of a few well-known objects.
*gitdir* is the path to the repository
'''
try:
status = git_filestatus(file, gitdir)
except subprocess.CalledProcessError as e:
print(e, file=sys.stderr)
return False
if already_controlled and status not in [ADDED, MODIFIED]:
return False
rc = git_fileadd(file, gitdir)
if not rc:
return rc == 0
rc = subprocess.call(COMMIT_CMD + [message, file], cwd=gitdir)
return rc == 0
[docs]def git_commit(message, gitdir, all=False):
'''
Commit all added files, using *message* as the commit message.
Return True upon success, False upon failure.
*gitdir* is the path to the repository
'''
rc = subprocess.call((COMMITALL_CMD if all else COMMIT_CMD) +
[message], cwd=gitdir)
return rc == 0
[docs]def git_reset(gitdir):
'''
Reset hard to clean up.
Return True upon success, False upon failure.
*gitdir* is the path to the repository
'''
rc = subprocess.call(RESET_CMD, cwd=gitdir)
return rc == 0
[docs]def git_lastlog(gitdir):
'''
Return last log message.
*gitdir* is the path to the repository
'''
try:
result = subprocess.check_output(LOG_CMD, cwd=gitdir).decode()
except subprocess.CalledProcessError as e:
result = str(e)
return result
[docs]def git_init(gitdir):
'''
Create and initialize a repository at path location *gitdir*
Return True upon success, False upon failure.
Fail complaining if *gitdir* exists.
'''
if os.path.exists(gitdir):
print('%s already exists' % (gitdir), file=sys.stderr)
return False
os.makedirs(gitdir)
rc = subprocess.call(INIT_CMD, cwd=gitdir)
return rc == 0