Source code for gittools

# 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