summaryrefslogtreecommitdiff
path: root/gitrepo.py
diff options
context:
space:
mode:
Diffstat (limited to 'gitrepo.py')
-rw-r--r--gitrepo.py234
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()