stgit.el: Use "->" for defstruct field accessors
[stgit] / stgit / git.py
index 0cfcd42..0316342 100644 (file)
@@ -18,7 +18,7 @@ along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
-import sys, os, re, gitmergeonefile
+import sys, os, re
 from shutil import copyfile
 
 from stgit.exception import *
 from shutil import copyfile
 
 from stgit.exception import *
@@ -153,14 +153,12 @@ def get_commit(id_hash):
 def get_conflicts():
     """Return the list of file conflicts
     """
 def get_conflicts():
     """Return the list of file conflicts
     """
-    names = []
+    names = set()
     for line in GRun('ls-files', '-z', '--unmerged'
                      ).raw_output().split('\0')[:-1]:
         stat, path = line.split('\t', 1)
     for line in GRun('ls-files', '-z', '--unmerged'
                      ).raw_output().split('\0')[:-1]:
         stat, path = line.split('\t', 1)
-        # Look for entries in stage 2 (could equally well use 3)
-        if stat.endswith(' 2'):
-            names.append(path)
-    return names
+        names.add(path)
+    return list(names)
 
 def exclude_files():
     files = [os.path.join(basedir.get(), 'info', 'exclude')]
 
 def exclude_files():
     files = [os.path.join(basedir.get(), 'info', 'exclude')]
@@ -169,7 +167,7 @@ def exclude_files():
         files.append(user_exclude)
     return files
 
         files.append(user_exclude)
     return files
 
-def ls_files(files, tree = None, full_name = True):
+def ls_files(files, tree = 'HEAD', full_name = True):
     """Return the files known to GIT or raise an error otherwise. It also
     converts the file to the full path relative the the .git directory.
     """
     """Return the files known to GIT or raise an error otherwise. It also
     converts the file to the full path relative the the .git directory.
     """
@@ -184,14 +182,32 @@ def ls_files(files, tree = None, full_name = True):
     args.append('--')
     args.extend(files)
     try:
     args.append('--')
     args.extend(files)
     try:
-        return GRun('ls-files', '--error-unmatch', *args).output_lines()
+        # use a set to avoid file names duplication due to different stages
+        fileset = set(GRun('ls-files', '--error-unmatch', *args).output_lines())
     except GitRunException:
         # just hide the details of the 'git ls-files' command we use
         raise GitException, \
             'Some of the given paths are either missing or not known to GIT'
     except GitRunException:
         # just hide the details of the 'git ls-files' command we use
         raise GitException, \
             'Some of the given paths are either missing or not known to GIT'
+    return list(fileset)
+
+def parse_git_ls(output):
+    """Parse the output of git diff-index, diff-files, etc. Doesn't handle
+    rename/copy output, so don't feed it output generated with the -M
+    or -C flags."""
+    t = None
+    for line in output.split('\0'):
+        if not line:
+            # There's a zero byte at the end of the output, which
+            # gives us an empty string as the last "line".
+            continue
+        if t == None:
+            mode_a, mode_b, sha1_a, sha1_b, t = line.split(' ')
+        else:
+            yield (t, line)
+            t = None
 
 def tree_status(files = None, tree_id = 'HEAD', unknown = False,
 
 def tree_status(files = None, tree_id = 'HEAD', unknown = False,
-                  noexclude = True, verbose = False, diff_flags = []):
+                  noexclude = True, verbose = False):
     """Get the status of all changed files, or of a selected set of
     files. Returns a list of pairs - (status, filename).
 
     """Get the status of all changed files, or of a selected set of
     files. Returns a list of pairs - (status, filename).
 
@@ -206,6 +222,8 @@ def tree_status(files = None, tree_id = 'HEAD', unknown = False,
 
     refresh_index()
 
 
     refresh_index()
 
+    if files is None:
+        files = []
     cache_files = []
 
     # unknown files
     cache_files = []
 
     # unknown files
@@ -228,26 +246,37 @@ def tree_status(files = None, tree_id = 'HEAD', unknown = False,
     cache_files += [('C', filename) for filename in conflicts
                     if not files or filename in files]
     reported_files = set(conflicts)
     cache_files += [('C', filename) for filename in conflicts
                     if not files or filename in files]
     reported_files = set(conflicts)
+    files_left = [f for f in files if f not in reported_files]
 
 
-    # files in the index
-    args = diff_flags + [tree_id]
-    if files:
-        args += ['--'] + files
-    for line in GRun('diff-index', *args).output_lines():
-        fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
-        if fs[1] not in reported_files:
-            cache_files.append(fs)
-            reported_files.add(fs[1])
-
-    # files in the index but changed on (or removed from) disk
-    args = list(diff_flags)
-    if files:
-        args += ['--'] + files
-    for line in GRun('diff-files', *args).output_lines():
-        fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
-        if fs[1] not in reported_files:
-            cache_files.append(fs)
-            reported_files.add(fs[1])
+    # files in the index. Only execute this code if no files were
+    # specified when calling the function (i.e. report all files) or
+    # files were specified but already found in the previous step
+    if not files or files_left:
+        args = [tree_id]
+        if files_left:
+            args += ['--'] + files_left
+        for t, fn in parse_git_ls(GRun('diff-index', '-z', *args).raw_output()):
+            # the condition is needed in case files is emtpy and
+            # diff-index lists those already reported
+            if not fn in reported_files:
+                cache_files.append((t, fn))
+                reported_files.add(fn)
+        files_left = [f for f in files if f not in reported_files]
+
+    # files in the index but changed on (or removed from) disk. Only
+    # execute this code if no files were specified when calling the
+    # function (i.e. report all files) or files were specified but
+    # already found in the previous step
+    if not files or files_left:
+        args = []
+        if files_left:
+            args += ['--'] + files_left
+        for t, fn in parse_git_ls(GRun('diff-files', '-z', *args).raw_output()):
+            # the condition is needed in case files is empty and
+            # diff-files lists those already reported
+            if not fn in reported_files:
+                cache_files.append((t, fn))
+                reported_files.add(fn)
 
     if verbose:
         out.done()
 
     if verbose:
         out.done()
@@ -443,109 +472,6 @@ def rename_branch(from_name, to_name):
            and os.path.exists(os.path.join(reflog_dir, from_name)):
         rename(reflog_dir, from_name, to_name)
 
            and os.path.exists(os.path.join(reflog_dir, from_name)):
         rename(reflog_dir, from_name, to_name)
 
-def add(names):
-    """Add the files or recursively add the directory contents
-    """
-    # generate the file list
-    files = []
-    for i in names:
-        if not os.path.exists(i):
-            raise GitException, 'Unknown file or directory: %s' % i
-
-        if os.path.isdir(i):
-            # recursive search. We only add files
-            for root, dirs, local_files in os.walk(i):
-                for name in [os.path.join(root, f) for f in local_files]:
-                    if os.path.isfile(name):
-                        files.append(os.path.normpath(name))
-        elif os.path.isfile(i):
-            files.append(os.path.normpath(i))
-        else:
-            raise GitException, '%s is not a file or directory' % i
-
-    if files:
-        try:
-            GRun('update-index', '--add', '--').xargs(files)
-        except GitRunException:
-            raise GitException, 'Unable to add file'
-
-def __copy_single(source, target, target2=''):
-    """Copy file or dir named 'source' to name target+target2"""
-
-    # "source" (file or dir) must match one or more git-controlled file
-    realfiles = GRun('ls-files', source).output_lines()
-    if len(realfiles) == 0:
-        raise GitException, '"%s" matches no git-controled files' % source
-
-    if os.path.isdir(source):
-        # physically copy the files, and record them to add them in one run
-        newfiles = []
-        re_string='^'+source+'/(.*)$'
-        prefix_regexp = re.compile(re_string)
-        for f in [f.strip() for f in realfiles]:
-            m = prefix_regexp.match(f)
-            if not m:
-                raise Exception, '"%s" does not match "%s"' % (f, re_string)
-            newname = target+target2+'/'+m.group(1)
-            if not os.path.exists(os.path.dirname(newname)):
-                os.makedirs(os.path.dirname(newname))
-            copyfile(f, newname)
-            newfiles.append(newname)
-
-        add(newfiles)
-    else: # files, symlinks, ...
-        newname = target+target2
-        copyfile(source, newname)
-        add([newname])
-
-
-def copy(filespecs, target):
-    if os.path.isdir(target):
-        # target is a directory: copy each entry on the command line,
-        # with the same name, into the target
-        target = target.rstrip('/')
-        
-        # first, check that none of the children of the target
-        # matching the command line aleady exist
-        for filespec in filespecs:
-            entry = target+ '/' + os.path.basename(filespec.rstrip('/'))
-            if os.path.exists(entry):
-                raise GitException, 'Target "%s" already exists' % entry
-        
-        for filespec in filespecs:
-            filespec = filespec.rstrip('/')
-            basename = '/' + os.path.basename(filespec)
-            __copy_single(filespec, target, basename)
-
-    elif os.path.exists(target):
-        raise GitException, 'Target "%s" exists but is not a directory' % target
-    elif len(filespecs) != 1:
-        raise GitException, 'Cannot copy more than one file to non-directory'
-
-    else:
-        # at this point: len(filespecs)==1 and target does not exist
-
-        # check target directory
-        targetdir = os.path.dirname(target)
-        if targetdir != '' and not os.path.isdir(targetdir):
-            raise GitException, 'Target directory "%s" does not exist' % targetdir
-
-        __copy_single(filespecs[0].rstrip('/'), target)
-        
-
-def rm(files, force = False):
-    """Remove a file from the repository
-    """
-    if not force:
-        for f in files:
-            if os.path.exists(f):
-                raise GitException, '%s exists. Remove it first' %f
-        if files:
-            GRun('update-index', '--remove', '--').xargs(files)
-    else:
-        if files:
-            GRun('update-index', '--force-remove', '--').xargs(files)
-
 # Persons caching
 __user = None
 __author = None
 # Persons caching
 __user = None
 __author = None
@@ -706,19 +632,23 @@ def merge_recursive(base, head1, head2):
     output = p.output_lines()
     if p.exitcode:
         # There were conflicts
     output = p.output_lines()
     if p.exitcode:
         # There were conflicts
-        conflicts = [l.strip() for l in output if l.startswith('CONFLICT')]
+        if config.get('stgit.autoimerge') == 'yes':
+            mergetool()
+        else:
+            conflicts = [l for l in output if l.startswith('CONFLICT')]
+            out.info(*conflicts)
+            raise GitException, "%d conflict(s)" % len(conflicts)
+
+def mergetool(files = ()):
+    """Invoke 'git mergetool' to resolve any outstanding conflicts. If 'not
+    files', all the files in an unmerged state will be processed."""
+    GRun('mergetool', *list(files)).returns([0, 1]).run()
+    # check for unmerged entries (prepend 'CONFLICT ' for consistency with
+    # merge_recursive())
+    conflicts = ['CONFLICT ' + f for f in get_conflicts()]
+    if conflicts:
         out.info(*conflicts)
         out.info(*conflicts)
-
-        # try the interactive merge or stage checkout (if enabled)
-        for filename in get_conflicts():
-            if (gitmergeonefile.merge(filename)):
-                # interactive merge succeeded
-                resolved([filename])
-
-        # any conflicts left unsolved?
-        cn = len(get_conflicts())
-        if cn:
-            raise GitException, "%d conflict(s)" % cn
+        raise GitException, "%d conflict(s)" % len(conflicts)
 
 def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = [],
          binary = True):
 
 def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = [],
          binary = True):
@@ -743,13 +673,6 @@ def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = [],
     else:
         return ''
 
     else:
         return ''
 
-# TODO: take another parameter representing a diff string as we
-# usually invoke git.diff() form the calling functions
-def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
-    """Return the diffstat between rev1 and rev2."""
-    return GRun('apply', '--stat', '--summary'
-                ).raw_input(diff(files, rev1, rev2)).raw_output()
-
 def files(rev1, rev2, diff_flags = []):
     """Return the files modified between rev1 and rev2
     """
 def files(rev1, rev2, diff_flags = []):
     """Return the files modified between rev1 and rev2
     """
@@ -835,7 +758,6 @@ def resolved(filenames, reset = None):
              '--stdin', '-z').input_nulterm(filenames).no_output()
     GRun('update-index', '--add', '--').xargs(filenames)
     for filename in filenames:
              '--stdin', '-z').input_nulterm(filenames).no_output()
     GRun('update-index', '--add', '--').xargs(filenames)
     for filename in filenames:
-        gitmergeonefile.clean_up(filename)
         # update the access and modificatied times
         os.utime(filename, None)
 
         # update the access and modificatied times
         os.utime(filename, None)
 
@@ -896,7 +818,7 @@ def repack():
     GRun('repack', '-a', '-d', '-f').run()
 
 def apply_patch(filename = None, diff = None, base = None,
     GRun('repack', '-a', '-d', '-f').run()
 
 def apply_patch(filename = None, diff = None, base = None,
-                fail_dump = True):
+                reject = False, strip = None):
     """Apply a patch onto the current or given index. There must not
     be any local changes in the tree, otherwise the command fails
     """
     """Apply a patch onto the current or given index. There must not
     be any local changes in the tree, otherwise the command fails
     """
@@ -915,19 +837,17 @@ def apply_patch(filename = None, diff = None, base = None,
     else:
         refresh_index()
 
     else:
         refresh_index()
 
+    cmd = ['apply', '--index']
+    if reject:
+        cmd += ['--reject']
+    if strip != None:
+        cmd += ['-p%s' % (strip,)]
     try:
     try:
-        GRun('apply', '--index').raw_input(diff).no_output()
+        GRun(*cmd).raw_input(diff).no_output()
     except GitRunException:
         if base:
             switch(orig_head)
     except GitRunException:
         if base:
             switch(orig_head)
-        if fail_dump:
-            # write the failed diff to a file
-            f = file('.stgit-failed.patch', 'w+')
-            f.write(diff)
-            f.close()
-            out.warn('Diff written to the .stgit-failed.patch file')
-
-        raise
+        raise GitException('Diff does not apply cleanly')
 
     if base:
         top = commit(message = 'temporary commit used for applying a patch',
 
     if base:
         top = commit(message = 'temporary commit used for applying a patch',
@@ -959,7 +879,6 @@ def refspec_remotepart(refspec):
         return m.group(1)
     else:
         raise GitException, 'Cannot parse refspec "%s"' % line
         return m.group(1)
     else:
         raise GitException, 'Cannot parse refspec "%s"' % line
-    
 
 def __remotes_from_config():
     return config.sections_matching(r'remote\.(.*)\.url')
 
 def __remotes_from_config():
     return config.sections_matching(r'remote\.(.*)\.url')