Add file renaming support
[stgit] / stgit / git.py
index 716609c..20cac61 100644 (file)
@@ -22,6 +22,7 @@ import sys, os, popen2, re, gitmergeonefile
 
 from stgit import basedir
 from stgit.utils import *
+from stgit.config import config
 
 # git exception class
 class GitException(Exception):
@@ -39,7 +40,6 @@ class Commit:
         self.__id_hash = id_hash
 
         lines = _output_lines('git-cat-file commit %s' % id_hash)
-        self.__parents = []
         for i in range(len(lines)):
             line = lines[i]
             if line == '\n':
@@ -47,8 +47,6 @@ class Commit:
             field = line.strip().split(' ', 1)
             if field[0] == 'tree':
                 self.__tree = field[1]
-            elif field[0] == 'parent':
-                self.__parents.append(field[1])
             if field[0] == 'author':
                 self.__author = field[1]
             if field[0] == 'committer':
@@ -62,10 +60,15 @@ class Commit:
         return self.__tree
 
     def get_parent(self):
-        return self.__parents[0]
+        parents = self.get_parents()
+        if parents:
+            return parents[0]
+        else:
+            return None
 
     def get_parents(self):
-        return self.__parents
+        return _output_lines('git-rev-list --parents --max-count=1 %s'
+                             % self.__id_hash)[0].split()[1:]
 
     def get_author(self):
         return self.__author
@@ -76,6 +79,9 @@ class Commit:
     def get_log(self):
         return self.__log
 
+    def __str__(self):
+        return self.get_id_hash()
+
 # dictionary of Commit objects, used to avoid multiple calls to git
 __commits = dict()
 
@@ -117,13 +123,23 @@ def _input(cmd, file_desc):
         p.tochild.write(line)
     p.tochild.close()
     if p.wait():
-        raise GitException, '%s failed' % str(cmd)
+        raise GitException, '%s failed (%s)' % (str(cmd),
+                                                p.childerr.read().strip())
+
+def _input_str(cmd, string):
+    p = popen2.Popen3(cmd, True)
+    p.tochild.write(string)
+    p.tochild.close()
+    if p.wait():
+        raise GitException, '%s failed (%s)' % (str(cmd),
+                                                p.childerr.read().strip())
 
 def _output(cmd):
     p=popen2.Popen3(cmd, True)
     output = p.fromchild.read()
     if p.wait():
-        raise GitException, '%s failed' % str(cmd)
+        raise GitException, '%s failed (%s)' % (str(cmd),
+                                                p.childerr.read().strip())
     return output
 
 def _output_one_line(cmd, file_desc = None):
@@ -134,14 +150,16 @@ def _output_one_line(cmd, file_desc = None):
         p.tochild.close()
     output = p.fromchild.readline().strip()
     if p.wait():
-        raise GitException, '%s failed' % str(cmd)
+        raise GitException, '%s failed (%s)' % (str(cmd),
+                                                p.childerr.read().strip())
     return output
 
 def _output_lines(cmd):
     p=popen2.Popen3(cmd, True)
     lines = p.fromchild.readlines()
     if p.wait():
-        raise GitException, '%s failed' % str(cmd)
+        raise GitException, '%s failed (%s)' % (str(cmd),
+                                                p.childerr.read().strip())
     return lines
 
 def __run(cmd, args=None):
@@ -164,9 +182,13 @@ def __run(cmd, args=None):
     return 0
 
 def __tree_status(files = None, tree_id = 'HEAD', unknown = False,
-                  noexclude = True):
+                  noexclude = True, verbose = False):
     """Returns a list of pairs - [status, filename]
     """
+    if verbose:
+        print 'Checking for changes in the working directory...',
+        sys.stdout.flush()
+
     refresh_index()
 
     if not files:
@@ -198,17 +220,20 @@ def __tree_status(files = None, tree_id = 'HEAD', unknown = False,
     cache_files += [('C', filename) for filename in conflicts]
 
     # the rest
-    for line in _output_lines(['git-diff-index', tree_id] + files):
+    for line in _output_lines(['git-diff-index', tree_id, '--'] + files):
         fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
         if fs[1] not in conflicts:
             cache_files.append(fs)
 
+    if verbose:
+        print 'done'
+
     return cache_files
 
 def local_changes():
     """Return true if there are local changes in the tree
     """
-    return len(__tree_status()) != 0
+    return len(__tree_status(verbose = True)) != 0
 
 # HEAD value cached
 __head = None
@@ -383,7 +408,7 @@ def update_cache(files = None, force = False):
     if not files:
         files = []
 
-    cache_files = __tree_status(files)
+    cache_files = __tree_status(files, verbose = False)
 
     # everything is up-to-date
     if len(cache_files) == 0:
@@ -426,7 +451,9 @@ def commit(message, files = None, parents = None, allowempty = False,
             raise GitException, 'No changes to commit'
 
     # get the commit message
-    if message[-1:] != '\n':
+    if not message:
+        message = '\n'
+    elif message[-1:] != '\n':
         message += '\n'
 
     must_switch = True
@@ -460,7 +487,7 @@ def commit(message, files = None, parents = None, allowempty = False,
 
     return commit_id
 
-def apply_diff(rev1, rev2, check_index = True):
+def apply_diff(rev1, rev2, check_index = True, files = None):
     """Apply the diff between rev1 and rev2 onto the current
     index. This function doesn't need to raise an exception since it
     is only used for fast-pushing a patch. If this operation fails,
@@ -470,18 +497,30 @@ def apply_diff(rev1, rev2, check_index = True):
         index_opt = '--index'
     else:
         index_opt = ''
-    cmd = 'git-diff-tree -p %s %s | git-apply %s 2> /dev/null' \
-          % (rev1, rev2, index_opt)
 
-    return os.system(cmd) == 0
+    if not files:
+        files = []
+
+    diff_str = diff(files, rev1, rev2)
+    if diff_str:
+        try:
+            _input_str('git-apply %s' % index_opt, diff_str)
+        except GitException:
+            return False
+
+    return True
 
 def merge(base, head1, head2):
     """Perform a 3-way merge between base, head1 and head2 into the
     local tree
     """
     refresh_index()
-    if __run('git-read-tree -u -m --aggressive', [base, head1, head2]) != 0:
-        raise GitException, 'git-read-tree failed (local changes maybe?)'
+
+    try:
+        # use _output() to mask the verbose prints of the tool
+        _output('git-merge-recursive %s -- %s %s' % (base, head1, head2))
+    except GitException:
+        pass
 
     # check the index for unmerged entries
     files = {}
@@ -551,13 +590,13 @@ def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
         files = []
 
     if rev1 and rev2:
-        diff_str = _output(['git-diff-tree', '-p', rev1, rev2] + files)
+        diff_str = _output(['git-diff-tree', '-p', rev1, rev2, '--'] + files)
     elif rev1 or rev2:
         refresh_index()
         if rev2:
-            diff_str = _output(['git-diff-index', '-p', '-R', rev2] + files)
+            diff_str = _output(['git-diff-index', '-p', '-R', rev2, '--'] + files)
         else:
-            diff_str = _output(['git-diff-index', '-p', rev1] + files)
+            diff_str = _output(['git-diff-index', '-p', rev1, '--'] + files)
     else:
         diff_str = ''
 
@@ -626,12 +665,13 @@ def checkout(files = None, tree_id = None, force = False):
     if __run(checkout_cmd, files) != 0:
         raise GitException, 'Failed git-checkout-index'
 
-def switch(tree_id):
+def switch(tree_id, keep = False):
     """Switch the tree to the given id
     """
-    refresh_index()
-    if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
-        raise GitException, 'git-read-tree failed (local changes maybe?)'
+    if not keep:
+        refresh_index()
+        if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
+            raise GitException, 'git-read-tree failed (local changes maybe?)'
 
     __set_head(tree_id)
 
@@ -666,34 +706,48 @@ def pull(repository = 'origin', refspec = None):
     if refspec:
         args.append(refspec)
 
-    if __run('git-pull', args) != 0:
+    if __run(config.get('stgit', 'pullcmd'), args) != 0:
         raise GitException, 'Failed "git-pull %s"' % repository
 
-def apply_patch(filename = None, base = None):
+def repack():
+    """Repack all objects into a single pack
+    """
+    __run('git-repack -a -d -f')
+
+def apply_patch(filename = None, diff = None, base = None,
+                fail_dump = True):
     """Apply a patch onto the current or given index. There must not
     be any local changes in the tree, otherwise the command fails
     """
-    def __apply_patch():
-        if filename:
-            return __run('git-apply --index', [filename]) == 0
-        else:
-            try:
-                _input('git-apply --index', sys.stdin)
-            except GitException:
-                return False
-            return True
-
     if base:
         orig_head = get_head()
         switch(base)
     else:
-        refresh_index()         # needed since __apply_patch() doesn't do it
+        refresh_index()
 
-    if not __apply_patch():
+    if diff is None:
+        if filename:
+            f = file(filename)
+        else:
+            f = sys.stdin
+        diff = f.read()
+        if filename:
+            f.close()
+
+    try:
+        _input_str('git-apply --index', diff)
+    except GitException:
         if base:
             switch(orig_head)
-        raise GitException, 'Patch does not apply cleanly'
-    elif base:
+        if fail_dump:
+            # write the failed diff to a file
+            f = file('.stgit-failed.patch', 'w+')
+            f.write(diff)
+            f.close()
+
+        raise
+
+    if base:
         top = commit(message = 'temporary commit used for applying a patch',
                      parents = [base])
         switch(orig_head)