From 49ed57c55e01dffe69668ae34deddd619b3486c8 Mon Sep 17 00:00:00 2001
From: Martin Braun <martin.braun@kit.edu>
Date: Sun, 24 Nov 2013 16:41:57 +0100
Subject: modtool: Added git support

---
 gr-utils/python/modtool/CMakeLists.txt     |   1 +
 gr-utils/python/modtool/modtool_add.py     |  48 ++++---
 gr-utils/python/modtool/modtool_base.py    |  21 ++-
 gr-utils/python/modtool/modtool_disable.py |   7 +
 gr-utils/python/modtool/modtool_makexml.py |  13 +-
 gr-utils/python/modtool/modtool_newmod.py  |   6 +
 gr-utils/python/modtool/modtool_rm.py      |   6 +
 gr-utils/python/modtool/scm.py             | 221 +++++++++++++++++++++++++++++
 8 files changed, 300 insertions(+), 23 deletions(-)
 create mode 100644 gr-utils/python/modtool/scm.py

(limited to 'gr-utils/python')

diff --git a/gr-utils/python/modtool/CMakeLists.txt b/gr-utils/python/modtool/CMakeLists.txt
index d7816816c3..bb4febe318 100644
--- a/gr-utils/python/modtool/CMakeLists.txt
+++ b/gr-utils/python/modtool/CMakeLists.txt
@@ -33,6 +33,7 @@ GR_PYTHON_INSTALL(FILES
     modtool_newmod.py
     modtool_rm.py
     parser_cc_block.py
+    scm.py
     templates.py
     util_functions.py
     DESTINATION ${GR_PYTHON_DIR}/gnuradio/modtool
diff --git a/gr-utils/python/modtool/modtool_add.py b/gr-utils/python/modtool/modtool_add.py
index 1be20f0485..e188b7fd71 100644
--- a/gr-utils/python/modtool/modtool_add.py
+++ b/gr-utils/python/modtool/modtool_add.py
@@ -135,8 +135,10 @@ class ModToolAdd(ModTool):
 
     def _write_tpl(self, tpl, path, fname):
         """ Shorthand for writing a substituted template to a file"""
-        print "Adding file '%s'..." % fname
-        open(os.path.join(path, fname), 'w').write(get_template(tpl, **self._info))
+        path_to_file = os.path.join(path, fname)
+        print "Adding file '%s'..." % path_to_file
+        open(path_to_file, 'w').write(get_template(tpl, **self._info))
+        self.scm.add_files((path_to_file,))
 
     def run(self):
         """ Go, go, go. """
@@ -185,6 +187,7 @@ class ModToolAdd(ModTool):
                                             '  s->addTest(gr::%s::qa_%s::suite());' % (self._info['modname'],
                                                                                        self._info['blockname'])
                                             )
+                    self.scm.mark_files_updated((self._file['qalib'],))
                 except IOError:
                     print "Can't add C++ QA files."
         def _add_qa36():
@@ -193,16 +196,16 @@ class ModToolAdd(ModTool):
             self._write_tpl('qa_cpp36', 'lib', fname_qa_cc)
             if not self._skip_cmakefiles:
                 open(self._file['cmlib'], 'a').write(
-                        str(
-                            Cheetah.Template.Template(
-                                Templates['qa_cmakeentry36'],
-                                searchList={'basename': os.path.splitext(fname_qa_cc)[0],
-                                            'upperbasename': os.path.splitext(fname_qa_cc)[0].upper(),
-                                            'filename': fname_qa_cc,
-                                            'modname': self._info['modname']
-                                           }
-                            )
-                         )
+                    str(
+                        Cheetah.Template.Template(
+                            Templates['qa_cmakeentry36'],
+                            searchList={'basename': os.path.splitext(fname_qa_cc)[0],
+                                        'upperbasename': os.path.splitext(fname_qa_cc)[0].upper(),
+                                        'filename': fname_qa_cc,
+                                        'modname': self._info['modname']
+                                       }
+                        )
+                     )
                 )
                 ed = CMakeFileEditor(self._file['cmlib'])
                 ed.remove_double_newlines()
@@ -223,6 +226,13 @@ class ModToolAdd(ModTool):
             fname_cc = self._info['fullblockname'] + '.cc'
             self._write_tpl('block_h36',   self._info['includedir'], fname_h)
             self._write_tpl('block_cpp36', 'lib',                    fname_cc)
+        if self._add_cc_qa:
+            if self._info['version'] == '37':
+                _add_qa()
+            elif self._info['version'] == '36':
+                _add_qa36()
+            elif self._info['version'] == 'autofoo':
+                print "Warning: C++ QA files not supported for autotools."
         if not self._skip_cmakefiles:
             ed = CMakeFileEditor(self._file['cmlib'])
             cmake_list_var = '[a-z]*_?' + self._info['modname'] + '_sources'
@@ -232,13 +242,7 @@ class ModToolAdd(ModTool):
             ed = CMakeFileEditor(self._file['cminclude'])
             ed.append_value('install', fname_h, to_ignore_end='DESTINATION[^()]+')
             ed.write()
-        if self._add_cc_qa:
-            if self._info['version'] == '37':
-                _add_qa()
-            elif self._info['version'] == '36':
-                _add_qa36()
-            elif self._info['version'] == 'autofoo':
-                print "Warning: C++ QA files not supported for autotools."
+            self.scm.mark_files_updated((self._file['cminclude'], self._file['cmlib']))
 
     def _run_swig(self):
         """ Do everything that needs doing in the subdir 'swig'.
@@ -264,6 +268,7 @@ class ModToolAdd(ModTool):
             regexp = re.compile('^%\{\n', re.MULTILINE)
             oldfile = regexp.sub('%%{\n%s\n' % include_str, oldfile, count=1)
             open(self._file['swig'], 'w').write(oldfile)
+        self.scm.mark_files_updated((self._file['swig'],))
 
     def _run_python_qa(self):
         """ Do everything that needs doing in the subdir 'python' to add
@@ -274,12 +279,14 @@ class ModToolAdd(ModTool):
         fname_py_qa = 'qa_' + self._info['blockname'] + '.py'
         self._write_tpl('qa_python', self._info['pydir'], fname_py_qa)
         os.chmod(os.path.join(self._info['pydir'], fname_py_qa), 0755)
+        self.scm.mark_files_updated((os.path.join(self._info['pydir'], fname_py_qa),))
         if self._skip_cmakefiles or CMakeFileEditor(self._file['cmpython']).check_for_glob('qa_*.py'):
             return
         print "Editing %s/CMakeLists.txt..." % self._info['pydir']
         open(self._file['cmpython'], 'a').write(
                 'GR_ADD_TEST(qa_%s ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/%s)\n' % \
                   (self._info['blockname'], fname_py_qa))
+        self.scm.mark_files_updated((self._file['cmpython'],))
 
     def _run_python(self):
         """ Do everything that needs doing in the subdir 'python' to add
@@ -293,11 +300,13 @@ class ModToolAdd(ModTool):
         append_re_line_sequence(self._file['pyinit'],
                                 '(^from.*import.*\n|# import any pure.*\n)',
                                 'from %s import %s' % (self._info['blockname'], self._info['blockname']))
+        self.scm.mark_files_updated((self._file['pyinit'],))
         if self._skip_cmakefiles:
             return
         ed = CMakeFileEditor(self._file['cmpython'])
         ed.append_value('GR_PYTHON_INSTALL', fname_py, to_ignore_end='DESTINATION[^()]+')
         ed.write()
+        self.scm.mark_files_updated((self._file['cmpython'],))
 
     def _run_grc(self):
         """ Do everything that needs doing in the subdir 'grc' to add
@@ -313,4 +322,5 @@ class ModToolAdd(ModTool):
         print "Editing grc/CMakeLists.txt..."
         ed.append_value('install', fname_grc, to_ignore_end='DESTINATION[^()]+')
         ed.write()
+        self.scm.mark_files_updated((self._file['cmgrc'],))
 
diff --git a/gr-utils/python/modtool/modtool_base.py b/gr-utils/python/modtool/modtool_base.py
index 768bce0f77..a58b7abcb1 100644
--- a/gr-utils/python/modtool/modtool_base.py
+++ b/gr-utils/python/modtool/modtool_base.py
@@ -24,16 +24,17 @@ import os
 import re
 from optparse import OptionParser, OptionGroup
 
+from gnuradio import gr
 from util_functions import get_modname
-
+from scm import SCMRepoFactory
 
 class ModToolException(BaseException):
     """ Standard exception for modtool classes. """
     pass
 
-
 class ModTool(object):
     """ Base class for all modtool command classes. """
+    name = 'base'
     def __init__(self):
         self._subdirs = ['lib', 'include', 'python', 'swig', 'grc'] # List subdirs where stuff happens
         self._has_subdirs = {}
@@ -68,6 +69,9 @@ class ModTool(object):
                 help="Don't do anything in the python/ subdirectory.")
         ogroup.add_option("--skip-grc", action="store_true", default=False,
                 help="Don't do anything in the grc/ subdirectory.")
+        ogroup.add_option("--scm-mode", type="choice", choices=('yes', 'no', 'auto'),
+                default=gr.prefs().get_string('modtool', 'scm_mode', 'no'),
+                help="Use source control management (yes, no or auto).")
         ogroup.add_option("-y", "--yes", action="store_true", default=False,
                 help="Answer all questions with 'yes'. This can overwrite and delete your files, so be careful.")
         parser.add_option_group(ogroup)
@@ -101,6 +105,8 @@ class ModTool(object):
         self._info['blockname'] = options.block_name
         self._setup_files()
         self._info['yes'] = options.yes
+        self.options = options
+        self._setup_scm()
 
     def _setup_files(self):
         """ Initialise the self._file[] dictionary """
@@ -124,6 +130,17 @@ class ModTool(object):
         self._file['cmswig'] = os.path.join('swig', 'CMakeLists.txt')
         self._file['cmfind'] = os.path.join('cmake', 'Modules', 'howtoConfig.cmake')
 
+
+    def _setup_scm(self, mode='active'):
+        """ Initialize source control management. """
+        if mode == 'active':
+            self.scm = SCMRepoFactory(self.options, '.').make_active_scm_manager()
+        else:
+            self.scm = SCMRepoFactory(self.options, '.').make_empty_scm_manager()
+        if self.scm is None:
+            print "Error: Can't set up SCM."
+            exit(1)
+
     def _check_directory(self, directory):
         """ Guesses if dir is a valid GNU Radio module directory by looking for
         CMakeLists.txt and at least one of the subdirs lib/, python/ and swig/.
diff --git a/gr-utils/python/modtool/modtool_disable.py b/gr-utils/python/modtool/modtool_disable.py
index 0538b47142..4ae2242f99 100644
--- a/gr-utils/python/modtool/modtool_disable.py
+++ b/gr-utils/python/modtool/modtool_disable.py
@@ -53,6 +53,7 @@ class ModToolDisable(ModTool):
         def _handle_py_qa(cmake, fname):
             """ Do stuff for py qa """
             cmake.comment_out_lines('GR_ADD_TEST.*'+fname)
+            self.scm.mark_file_updated(cmake.filename)
             return True
         def _handle_py_mod(cmake, fname):
             """ Do stuff for py extra files """
@@ -64,6 +65,7 @@ class ModToolDisable(ModTool):
             pymodname = os.path.splitext(fname)[0]
             initfile = re.sub(r'((from|import)\s+\b'+pymodname+r'\b)', r'#\1', initfile)
             open(self._file['pyinit'], 'w').write(initfile)
+            self.scm.mark_file_updated(self._file['pyinit'])
             return False
         def _handle_cc_qa(cmake, fname):
             """ Do stuff for cc qa """
@@ -74,10 +76,12 @@ class ModToolDisable(ModTool):
                 ed.comment_out_lines('#include\s+"%s.h"' % fname_base, comment_str='//')
                 ed.comment_out_lines('%s::suite\(\)' % fname_base, comment_str='//')
                 ed.write()
+                self.scm.mark_file_updated(self._file['qalib'])
             elif self._info['version'] == '36':
                 cmake.comment_out_lines('add_executable.*'+fname)
                 cmake.comment_out_lines('target_link_libraries.*'+os.path.splitext(fname)[0])
                 cmake.comment_out_lines('GR_ADD_TEST.*'+os.path.splitext(fname)[0])
+            self.scm.mark_file_updated(cmake.filename)
             return True
         def _handle_h_swig(cmake, fname):
             """ Comment out include files from the SWIG file,
@@ -96,6 +100,7 @@ class ModToolDisable(ModTool):
                 if nsubs > 1:
                     print "Hm, changed more then expected while editing %s." % self._file['swig']
             open(self._file['swig'], 'w').write(swigfile)
+            self.scm.mark_file_updated(self._file['swig'])
             return False
         def _handle_i_swig(cmake, fname):
             """ Comment out include files from the SWIG file,
@@ -108,6 +113,7 @@ class ModToolDisable(ModTool):
             print "Changing %s..." % self._file['swig']
             swigfile = re.sub('(GR_SWIG_BLOCK_MAGIC2?.+'+blockname+'.+;)', r'//\1', swigfile)
             open(self._file['swig'], 'w').write(swigfile)
+            self.scm.mark_file_updated(self._file['swig'])
             return False
         # List of special rules: 0: subdir, 1: filename re match, 2: callback
         special_treatments = (
@@ -145,5 +151,6 @@ class ModToolDisable(ModTool):
                 if not file_disabled:
                     cmake.disable_file(fname)
             cmake.write()
+            self.scm.mark_files_updated((os.path.join(subdir, 'CMakeLists.txt'),))
         print "Careful: 'gr_modtool disable' does not resolve dependencies."
 
diff --git a/gr-utils/python/modtool/modtool_makexml.py b/gr-utils/python/modtool/modtool_makexml.py
index 4b67d1b062..28eabe1b81 100644
--- a/gr-utils/python/modtool/modtool_makexml.py
+++ b/gr-utils/python/modtool/modtool_makexml.py
@@ -66,6 +66,7 @@ class ModToolMakeXML(ModTool):
                 (params, iosig, blockname) = self._parse_cc_h(f)
                 self._make_grc_xml_from_block_data(params, iosig, blockname)
         # 2) Go through python/
+        # TODO
 
     def _search_files(self, path, path_glob):
         """ Search for files matching pattern in the given path. """
@@ -84,6 +85,7 @@ class ModToolMakeXML(ModTool):
         generator. Also, check the makefile if the .xml file is in there.
         If necessary, add. """
         fname_xml = '%s_%s.xml' % (self._info['modname'], blockname)
+        path_to_xml = os.path.join('grc', fname_xml)
         # Some adaptions for the GRC
         for inout in ('in', 'out'):
             if iosig[inout]['max_ports'] == '-1':
@@ -93,11 +95,13 @@ class ModToolMakeXML(ModTool):
                                'name': 'Num %sputs' % inout,
                                'default': '2',
                                'in_constructor': False})
-        if os.path.isfile(os.path.join('grc', fname_xml)):
+        file_exists = False
+        if os.path.isfile(path_to_xml):
             if not self._info['yes']:
                 if not ask_yes_no('Overwrite existing GRC file?', False):
                     return
             else:
+                file_exists = True
                 print "Warning: Overwriting existing GRC file."
         grc_generator = GRCXMLGenerator(
                 modname=self._info['modname'],
@@ -105,13 +109,18 @@ class ModToolMakeXML(ModTool):
                 params=params,
                 iosig=iosig
         )
-        grc_generator.save(os.path.join('grc', fname_xml))
+        grc_generator.save(path_to_xml)
+        if file_exists:
+            self.scm.mark_files_updated((path_to_xml,))
+        else:
+            self.scm.add_files((path_to_xml,))
         if not self._skip_subdirs['grc']:
             ed = CMakeFileEditor(self._file['cmgrc'])
             if re.search(fname_xml, ed.cfile) is None and not ed.check_for_glob('*.xml'):
                 print "Adding GRC bindings to grc/CMakeLists.txt..."
                 ed.append_value('install', fname_xml, to_ignore_end='DESTINATION[^()]+')
                 ed.write()
+                self.scm.mark_files_updated(self._file['cmgrc'])
 
     def _parse_cc_h(self, fname_cc):
         """ Go through a .cc and .h-file defining a block and return info """
diff --git a/gr-utils/python/modtool/modtool_newmod.py b/gr-utils/python/modtool/modtool_newmod.py
index 59818b1e62..3e05ecbf48 100644
--- a/gr-utils/python/modtool/modtool_newmod.py
+++ b/gr-utils/python/modtool/modtool_newmod.py
@@ -26,6 +26,7 @@ import re
 from optparse import OptionGroup
 from gnuradio import gr
 from modtool_base import ModTool, ModToolException
+from scm import SCMRepoFactory
 
 class ModToolNewModule(ModTool):
     """ Create a new out-of-tree module """
@@ -45,6 +46,7 @@ class ModToolNewModule(ModTool):
         return parser
 
     def setup(self, options, args):
+        # Don't call ModTool.setup(), that assumes an existing module.
         self._info['modname'] = options.module_name
         if self._info['modname'] is None:
             if len(args) >= 2:
@@ -67,6 +69,8 @@ class ModToolNewModule(ModTool):
         self._srcdir = gr.prefs().get_string('modtool', 'newmod_path', options.srcdir)
         if not os.path.isdir(self._srcdir):
             raise ModToolException('Could not find gr-newmod source dir.')
+        self.options = options
+        self._setup_scm(mode='new')
 
     def run(self):
         """
@@ -92,5 +96,7 @@ class ModToolNewModule(ModTool):
             if os.path.basename(root) == 'howto':
                 os.rename(root, os.path.join(os.path.dirname(root), self._info['modname']))
         print "Done."
+        if self.scm.init_repo(path_to_repo="."):
+            print "Created repository... you might want to commit before continuing."
         print "Use 'gr_modtool add' to add a new block to this currently empty module."
 
diff --git a/gr-utils/python/modtool/modtool_rm.py b/gr-utils/python/modtool/modtool_rm.py
index 4b69be180a..47128dbc87 100644
--- a/gr-utils/python/modtool/modtool_rm.py
+++ b/gr-utils/python/modtool/modtool_rm.py
@@ -66,16 +66,19 @@ class ModToolRemove(ModTool):
                                              '^\s*s->addTest\(gr::%s::%s::suite\(\)\);\s*$' % (
                                                     self._info['modname'], base)
                                             )
+                    self.scm.mark_file_updated(self._file['qalib'])
                 elif ext == '.cc':
                     ed.remove_value('list',
                                     '\$\{CMAKE_CURRENT_SOURCE_DIR\}/%s' % filename,
                                     to_ignore_start='APPEND test_%s_sources' % self._info['modname'])
+                    self.scm.mark_file_updated(ed.filename)
             else:
                 filebase = os.path.splitext(filename)[0]
                 ed.delete_entry('add_executable', filebase)
                 ed.delete_entry('target_link_libraries', filebase)
                 ed.delete_entry('GR_ADD_TEST', filebase)
                 ed.remove_double_newlines()
+                self.scm.mark_file_updated(ed.filename)
 
         def _remove_py_test_case(filename=None, ed=None):
             """ Special function that removes the occurrences of a qa*.{cc,h} file
@@ -103,6 +106,7 @@ class ModToolRemove(ModTool):
             for f in incl_files_deleted + swig_files_deleted:
                 # TODO do this on all *.i files
                 remove_pattern_from_file(self._file['swig'], _make_swig_regex(f))
+                self.scm.mark_file_updated(self._file['swig'])
         if not self._skip_subdirs['python']:
             py_files_deleted = self._run_subdir('python', ('*.py',), ('GR_PYTHON_INSTALL',),
                                                 cmakeedit_func=_remove_py_test_case)
@@ -148,6 +152,7 @@ class ModToolRemove(ModTool):
                     continue
             files_deleted.append(b)
             print "Deleting %s." % f
+            self.scm.remove_file(f)
             os.unlink(f)
             print "Deleting occurrences of %s from %s/CMakeLists.txt..." % (b, path)
             for var in makefile_vars:
@@ -155,4 +160,5 @@ class ModToolRemove(ModTool):
             if cmakeedit_func is not None:
                 cmakeedit_func(b, ed)
         ed.write()
+        self.scm.mark_files_updated(('%s/CMakeLists.txt' % path,))
         return files_deleted
diff --git a/gr-utils/python/modtool/scm.py b/gr-utils/python/modtool/scm.py
new file mode 100644
index 0000000000..ec6023ab3b
--- /dev/null
+++ b/gr-utils/python/modtool/scm.py
@@ -0,0 +1,221 @@
+#
+# Copyright 2013 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING.  If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+""" Class to handle source code management repositories. """
+
+import subprocess
+
+try:
+    import git
+    HAS_GITPYTHON = True
+except ImportError:
+    HAS_GITPYTHON = False
+# GitPython is a bit too unstable currently
+HAS_GITPYTHON = False
+
+class InvalidSCMError(Exception):
+    """ Exception for when trying to access a repo of wrong type. """
+    def __init__(self):
+        Exception.__init__(self)
+
+### Base class ###############################################################
+class SCMRepository(object):
+    """ Base class to handle interactions with source code management systems. """
+    handles_scm_type = '*'
+    def __init__(self, path_to_repo, is_empty=False):
+        self.path_to_repo = path_to_repo
+        self.is_empty = is_empty
+
+    def init_repo(self, path_to_repo=None, add_files=True):
+        """ Initialize the directory as a repository. Assumes the self.path_to_repo
+        (or path_to_repo, if specified) does *not* contain a valid repository.
+        If add_files is True, all files in this directory are added to version control.
+        Returns true if actually created a repo.
+        """
+        if path_to_repo is not None:
+            self.path_to_repo = path_to_repo
+        return False
+
+    def add_files(self, paths_to_files):
+        """ Add a tuple or list of files to the current repository. """
+        pass
+
+    def add_file(self, path_to_file):
+        """ Add a file to the current repository. """
+        self.add_files([path_to_file])
+
+    def remove_files(self, paths_to_files):
+        """ Remove a tuple or list of files from the current repository. """
+        pass
+
+    def remove_file(self, path_to_file):
+        """ Remove a file from the current repository. """
+        self.remove_files([path_to_file])
+
+    def mark_files_updated(self, paths_to_files):
+        """ Mark a list of tuple of files as changed. """
+        pass
+
+    def mark_file_updated(self, path_to_file):
+        """ Mark a file as changed. """
+        self.mark_files_updated([path_to_file])
+
+    def is_active(self):
+        """ Returns true if this repository manager is operating on an active, source-controlled directory. """
+        return self.is_empty
+
+
+### Git #####################################################################
+class GitManagerGitPython(object):
+    """ Manage git through GitPython (preferred way). """
+    def __init__(self, path_to_repo, init=False):
+        if init:
+            self.repo = git.Repo.init(path_to_repo, mkdir=False)
+        else:
+            try:
+                self.repo = git.Repo(path_to_repo)
+            except git.InvalidGitRepositoryError:
+                self.repo = None
+                raise InvalidSCMError
+        self.index = self.repo.index
+
+    def add_files(self, paths_to_files):
+        """ Adds a tuple of files to the index of the current repository. """
+        if self.repo is not None:
+            self.index.add(paths_to_files)
+
+    def remove_files(self, paths_to_files):
+        """ Removes a tuple of files from the index of the current repository. """
+        if self.repo is not None:
+            self.index.remove(paths_to_files)
+
+
+class GitManagerShell(object):
+    """ Call the git executable through a shell. """
+    def __init__(self, path_to_repo, init=False, git_executable=None):
+        self.path_to_repo = path_to_repo
+        if git_executable is None:
+            try:
+                self.git_executable = subprocess.check_output('which git', shell=True).strip()
+            except (OSError, subprocess.CalledProcessError):
+                raise InvalidSCMError
+        try:
+            if init:
+                subprocess.check_output([self.git_executable, 'init'])
+            else:
+                subprocess.check_output([self.git_executable, 'status'])
+        except OSError:
+            raise InvalidSCMError
+        except subprocess.CalledProcessError:
+            raise InvalidSCMError
+
+    def add_files(self, paths_to_files):
+        """ Adds a tuple of files to the index of the current repository. Does not commit. """
+        subprocess.check_output([self.git_executable, 'add'] + list(paths_to_files))
+
+    def remove_files(self, paths_to_files):
+        """ Removes a tuple of files from the index of the current repository. Does not commit. """
+        subprocess.check_output([self.git_executable, 'rm', '--cached'] + list(paths_to_files))
+
+
+class GitRepository(SCMRepository):
+    """ Specific to operating on git repositories. """
+    handles_scm_type = 'git'
+    def __init__(self, path_to_repo, is_empty=False):
+        SCMRepository.__init__(self, path_to_repo, is_empty)
+        if not is_empty:
+            try:
+                if HAS_GITPYTHON:
+                    self.repo_manager = GitManagerGitPython(path_to_repo)
+                else:
+                    self.repo_manager = GitManagerShell(path_to_repo)
+            except InvalidSCMError:
+                self.repo_manager = None
+        else:
+            self.repo_manager = None
+
+    def init_repo(self, path_to_repo=None, add_files=True):
+        """ Makes the directory in self.path_to_repo a git repo.
+        If add_file is True, all files in this dir are added to the index. """
+        SCMRepository.init_repo(self, path_to_repo, add_files)
+        if HAS_GITPYTHON:
+            self.repo_manager = GitManagerGitPython(self.path_to_repo, init=True)
+        else:
+            self.repo_manager = GitManagerShell(self.path_to_repo, init=True)
+        if add_files:
+            self.add_files(('*',))
+        return True
+
+    def add_files(self, paths_to_files):
+        """ Add a file to the current repository. Does not commit. """
+        self.repo_manager.add_files(paths_to_files)
+
+    def remove_files(self, paths_to_files):
+        """ Remove a file from the current repository. Does not commit. """
+        self.repo_manager.remove_files(paths_to_files)
+
+    def mark_files_updated(self, paths_to_files):
+        """ Mark a file as changed. Since this is git, same as adding new files. """
+        self.add_files(paths_to_files)
+
+    def is_active(self):
+        return self.repo_manager is not None
+
+
+##############################################################################
+### Factory ##################################################################
+class SCMRepoFactory(object):
+    """ Factory object to create the correct SCM class from the given options and dir. """
+    def __init__(self, options, path_to_repo):
+        self.path_to_repo = path_to_repo
+        self.options = options
+
+    def make_active_scm_manager(self):
+        """ Returns a valid, usable object of type SCMRepository. """
+        if self.options.scm_mode == 'no':
+            return SCMRepository(self.path_to_repo)
+        for glbl in globals().values():
+            try:
+                if issubclass(glbl, SCMRepository):
+                    the_scm = glbl(self.path_to_repo)
+                    if the_scm.is_active():
+                        print 'Found SCM of type:', the_scm.handles_scm_type
+                        return the_scm
+            except (TypeError, AttributeError, InvalidSCMError):
+                pass
+        if self.options == 'yes':
+            return None
+        return SCMRepository(self.path_to_repo)
+
+    def make_empty_scm_manager(self, scm_type='git'):
+        """ Returns a valid, usable object of type SCMRepository for an unitialized dir. """
+        if self.options.scm_mode == 'no':
+            return SCMRepository(self.path_to_repo)
+        for glbl in globals().values():
+            try:
+                if issubclass(glbl, SCMRepository):
+                    if glbl.handles_scm_type == scm_type:
+                        return glbl(self.path_to_repo, is_empty=True)
+            except (TypeError, AttributeError, InvalidSCMError):
+                pass
+        if self.options == 'yes':
+            return None
+        return SCMRepository(self.path_to_repo)
+
-- 
cgit v1.2.3