diff options
Diffstat (limited to 'gitrepo.py')
-rw-r--r-- | gitrepo.py | 234 |
1 files changed, 234 insertions, 0 deletions
diff --git a/gitrepo.py b/gitrepo.py new file mode 100644 index 0000000..06a79de --- /dev/null +++ b/gitrepo.py @@ -0,0 +1,234 @@ +import unittest +import logging +import os + +from sh import git +from sh import ErrorReturnCode +from sh import ErrorReturnCode_128 + +import subprocess + +# Use this to manage 'cd' context +from contextlib import contextmanager + +from proj import Proj +# Return to prevdir when context is exited. +# Use in the following manner: +# +# with cd("foodir"): +# operation1_in_foodir +# operation2_in_foodir +# +# operation3_in_prevdir +# +@contextmanager +def cd(newdir): + prevdir = os.getcwd() + os.chdir(os.path.expanduser(newdir)) + try: + yield + finally: + os.chdir(prevdir) + +# This is called 'GitRepo' because a repository for a project doesn't only have +# to be a git repository. Abstract base class used to define the operations +# that both Clone and Workdir share. Notice that there is no actual repodir +# specified here. You must instantiate a subclass in order to use/test these +# operations. This is because 'clones' and 'workdirs' are created differently +# but you operate on them in the exact same way. +class GitRepo(object): + def __init__(self, proj): + self.repodir=u"" + if isinstance(proj, Proj): + self.proj=proj + else: + raise TypeError('proj input parameter is not of type Proj') + + def branchexists(self, branch): + with cd(self.repodir): + try: + # Quote the branchname because it might have strange + # characters in it. + br="%s" % branch + git("rev-parse", "--verify", br) + except ErrorReturnCode_128: + return False + return True + + def getbranch(self): + try: + with cd(self.repodir): + branch=git("rev-parse", "--abbrev-ref", "HEAD").stdout.rstrip() + except ErrorReturnCode: + raise EnvironmentError("Unable to get the current branch") + return branch + + # TODO: Fully test this. + # Stash current changes and change to new branch. + # Return to previous branch when context is exited. + @contextmanager + def checkoutbranch(self, branch, track=""): + with cd(self.repodir): + # The stashid will remain empty if there were no changes in the + # directory to stash. + stashid=u"" + + try: + self.prevbranch=git("rev-parse", "--abbrev-ref", "HEAD").stdout.rstrip() + except ErrorReturnCode: + raise EnvironmentError("Unable to get the current branch") + + try: + # TODO: do we want to do a blanket git add *? + stashid=git("stash", "create").stdout.rstrip() + # If there's no stash then there's no reason to save. + if stashid!="": + git("stash", "save") + except ErrorReturnCode: + raise EnvironmentError("Unable to get the current branch") + + try: + # TODO This is a nop as it is. + git("rev-parse", "--verify", branch) + # if the branch exists the previous statement won't cause an + # exception so we know we can just check it out. + git("checkout", branch) + except ErrorReturnCode: + try: + # if it doesn't exist we need to checkout a new branch + git("checkout", "-b", branch, track).stdout.rstrip() + except ErrorReturnCode as exc: + raise EnvironmentError("Unable to checkout -b %s %s" % (branch, track)) + try: + yield + finally: + try: + with cd(self.repodir): + git("checkout", self.prevbranch) + # If there's no stashid then there were no files stashed, + # so don't stash apply. + if stashid!="": + git("stash", "apply", stashid) + except ErrorReturnCode, exc: + raise EnvironmentError("Unable to return to the previous branch: %s" % exc.stderr) + + # TODO: Write a unit test for this. + def add(self, filetogitadd): + # TODO: Determine if filetogitadd is relative or absolute. + # TODO: verify that filetogitadd is in self.repodir + try: + with cd(self.repodir): + git("add", filetogitadd) + except ErrorReturnCode: + raise EnvironmentError("Unable to git add " + filetogitadd) + + # TODO: Write a unit test for this. + # TODO: fix the default + def commit(self, message="default"): + print "Committing changes." + try: + with cd(self.repodir): + # Git commit first with a boiler plate message and then allow the user + # to amend. + if git("status", "--porcelain"): + # print git("commit", "-m", '%s' % message, _err_to_out=True) + subprocess.call(["git", "commit", "-m", message]) + # using python sh will suppress the git editor + subprocess.call(["git", "commit", "--amend"]) + except ErrorReturnCode: + raise EnvironmentError("Unable to git commit ") + + def log(self, number): + try: + with cd(self.repodir): + print git("log", "-n %d" % number) + except ErrorReturnCode: + raise EnvironmentError("Unable to git add " + filetogitadd) + + # TODO: Does this need to 'cd' first? + def editFile(self, toedit): + editor = os.getenv('EDITOR') + if not editor: + editor='/usr/bin/vim' + os.system(editor + ' ' + self.repodir + '/' + toedit) + + self.add(toedit) + + return + + # TODO: Test this function with both dryrun=True and dryrun=False + def pushToBranch(self, tobranch, dryrun=True): + try: + # TODO: Check for un-merged commits. + with cd(self.repodir): + branch=self.getbranch() + if dryrun: + git("push", "--dry-run", branch, tobranch) + else: + git("push", branch, tobranch) + except ErrorReturnCode: + raise EnvironmentError("Unable to push branch %s to %s" % (branch, tobranch)) + + def __str__(self): + return self.repodir + +# TestGitRepo is an abstract baseclass. Verify that the constructor +# will throw an exception if you try to run operations without +# defining a subclass. +class TestGitRepo(unittest.TestCase): + repoprefix="GitRepoUT" + + # This class is only used to test the GitRepo functions since, as an, + # abstract-baseclass, GitRepo doesn't define repodir, and we need an + # actual repository to test git based functions. + class DerivedGitRepo(GitRepo): + def __init__(self, proj): + super(TestGitRepo.DerivedGitRepo,self).__init__(proj) + self.remote=u'http://git.linaro.org/toolchain/release-notes.git' + dgr_dirname="release-notes" + self.repodir=self.proj.projdir + "/" + dgr_dirname + try: + with cd(self.proj.projdir): + git("clone", self.remote, dgr_dirname, _err_to_out=True, _out="/dev/null") + except ErrorReturnCode: + raise EnvironmentError("Git unable to clone into " + self.repodir) + + @classmethod + def setUpClass(cls): + cls.proj=Proj(prefix=TestGitRepo.repoprefix, persist=False) + cls.dgr=TestGitRepo.DerivedGitRepo(cls.proj) + + @classmethod + def tearDownClass(cls): + cls.proj.cleanup() + + # Test that the abstract baseclass throws an exception if one of the + # functions are called from the baseclass without a derived class. + def test_repo(self): + self.repo=GitRepo(self.proj) + with self.assertRaises(OSError): + # We haven't set a repodir so this should throw an exception. + branch=self.repo.getbranch() + + # TODO: Create a test-branch in the repo so it's always there. + + def test_branchexists(self): + self.assertTrue(self.dgr.branchexists("remotes/origin/releases/linaro-4.9-2015.05")) + + def test_not_branchexists(self): + self.assertFalse(self.dgr.branchexists("foobar")) + + def test_getbranch(self): + with cd(self.dgr.repodir): + try: + git("checkout", "-b", "master_test", "origin/master").stdout.rstrip() + except ErrorReturnCode as exc: + raise EnvironmentError("Unable to checkout -b %s %s" % (branch, track)) + self.assertEquals(self.dgr.getbranch(), "master_test") + + # TODO: Test checkoutbranch with various combinations of polluted + # directories. + +if __name__ == '__main__': + #logging.basicConfig(level="INFO") + unittest.main() |