Clear up the semantics of Series.new_patch
[stgit] / stgit / stack.py
index 12c5091..5e9d4fb 100644 (file)
@@ -19,6 +19,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os, re
+from email.Utils import formatdate
 
 from stgit.utils import *
 from stgit.out import *
@@ -66,6 +67,8 @@ def __clean_comments(f):
     f.seek(0); f.truncate()
     f.writelines(lines)
 
+# TODO: move this out of the stgit.stack module, it is really for
+# higher level commands to handle the user interaction
 def edit_file(series, line, comment, show_patch = True):
     fname = '.stgitmsg.txt'
     tmpl = templates.get_template('patchdescr.tmpl')
@@ -87,7 +90,8 @@ def edit_file(series, line, comment, show_patch = True):
     if show_patch:
        print >> f, __patch_prefix
        # series.get_patch(series.get_current()).get_top()
-       git.diff([], series.get_patch(series.get_current()).get_bottom(), None, f)
+       diff_str = git.diff(rev1 = series.get_patch(series.get_current()).get_bottom())
+       f.write(diff_str)
 
     #Vim modeline must be near the end.
     print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
@@ -250,7 +254,16 @@ class Patch(StgitObject):
         self._set_field('authemail', email or git.author().email)
 
     def get_authdate(self):
-        return self._get_field('authdate')
+        date = self._get_field('authdate')
+        if not date:
+            return date
+
+        if re.match('[0-9]+\s+[+-][0-9]+', date):
+            # Unix time (seconds) + time zone
+            secs_tz = date.split()
+            date = formatdate(int(secs_tz[0]))[:-5] + secs_tz[1]
+
+        return date
 
     def set_authdate(self, date):
         self._set_field('authdate', date or git.author().date)
@@ -347,6 +360,12 @@ class PatchSet(StgitObject):
         return bool(config.get(self.format_version_key()))
 
 
+def shortlog(patches):
+    log = ''.join(Run('git-log', '--pretty=short',
+                      p.get_top(), '^%s' % p.get_bottom()).raw_output()
+                  for p in patches)
+    return Run('git-shortlog').raw_input(log).raw_output()
+
 class Series(PatchSet):
     """Class including the operations on series
     """
@@ -725,13 +744,8 @@ class Series(PatchSet):
             except GitException:
                 out.warn('Could not delete branch "%s"' % self.get_name())
 
-        # Cleanup parent informations
-        # FIXME: should one day make use of git-config --section-remove,
-        # scheduled for 1.5.1
-        config.unset('branch.%s.remote' % self.get_name())
-        config.unset('branch.%s.merge' % self.get_name())
-        config.unset('branch.%s.stgit.parentbranch' % self.get_name())
-        config.unset(self.format_version_key())
+        config.remove_section('branch.%s' % self.get_name())
+        config.remove_section('branch.%s.stgit' % self.get_name())
 
     def refresh_patch(self, files = None, message = None, edit = False,
                       show_patch = False,
@@ -740,15 +754,13 @@ class Series(PatchSet):
                       author_date = None,
                       committer_name = None, committer_email = None,
                       backup = False, sign_str = None, log = 'refresh',
-                      notes = None):
-        """Generates a new commit for the given patch
+                      notes = None, bottom = None):
+        """Generates a new commit for the topmost patch
         """
-        name = self.get_current()
-        if not name:
+        patch = self.get_current_patch()
+        if not patch:
             raise StackException, 'No patches applied'
 
-        patch = self.get_patch(name)
-
         descr = patch.get_description()
         if not (message or descr):
             edit = True
@@ -756,10 +768,12 @@ class Series(PatchSet):
         elif message:
             descr = message
 
+        # TODO: move this out of the stgit.stack module, it is really
+        # for higher level commands to handle the user interaction
         if not message and edit:
             descr = edit_file(self, descr.rstrip(), \
                               'Please edit the description for patch "%s" ' \
-                              'above.' % name, show_patch)
+                              'above.' % patch.get_name(), show_patch)
 
         if not author_name:
             author_name = patch.get_authname()
@@ -772,16 +786,10 @@ class Series(PatchSet):
         if not committer_email:
             committer_email = patch.get_commemail()
 
-        if sign_str:
-            descr = descr.rstrip()
-            if descr.find("\nSigned-off-by:") < 0 \
-               and descr.find("\nAcked-by:") < 0:
-                descr = descr + "\n"
+        descr = add_sign_line(descr, sign_str, committer_name, committer_email)
 
-            descr = '%s\n%s: %s <%s>\n' % (descr, sign_str,
-                                           committer_name, committer_email)
-
-        bottom = patch.get_bottom()
+        if not bottom:
+            bottom = patch.get_bottom()
 
         commit_id = git.commit(files = files,
                                message = descr, parents = [bottom],
@@ -833,14 +841,23 @@ class Series(PatchSet):
                   author_name = None, author_email = None, author_date = None,
                   committer_name = None, committer_email = None,
                   before_existing = False):
-        """Creates a new patch
+        """Creates a new patch, either pointing to an existing commit object,
+        or by creating a new commit object.
         """
 
+        assert commit or (top and bottom)
+        assert not before_existing or (top and bottom)
+        assert not (commit and before_existing)
+        assert (top and bottom) or (not top and not bottom)
+        assert not top or (bottom == git.get_commit(top).get_parent())
+
         if name != None:
             self.__patch_name_valid(name)
             if self.patch_exists(name):
                 raise StackException, 'Patch "%s" already exists' % name
 
+        # TODO: move this out of the stgit.stack module, it is really
+        # for higher level commands to handle the user interaction
         if not message and can_edit:
             descr = edit_file(
                 self, None,
@@ -873,9 +890,6 @@ class Series(PatchSet):
 
         if before_existing:
             insert_string(self.__applied_file, patch.get_name())
-            # no need to commit anything as the object is already
-            # present (mainly used by 'uncommit')
-            commit = False
         elif unapplied:
             patches = [patch.get_name()] + self.get_unapplied()
             write_strings(self.__unapplied_file, patches)
@@ -900,6 +914,8 @@ class Series(PatchSet):
                                    committer_email = committer_email)
             # set the patch top to the new commit
             patch.set_top(commit_id)
+        else:
+            assert top != bottom
 
         self.log_patch(patch, 'new')
 
@@ -1021,7 +1037,23 @@ class Series(PatchSet):
 
         return merged
 
-    def push_patch(self, name, empty = False):
+    def push_empty_patch(self, name):
+        """Pushes an empty patch on the stack
+        """
+        unapplied = self.get_unapplied()
+        assert(name in unapplied)
+
+        # patch = self.get_patch(name)
+        head = git.get_head()
+
+        append_string(self.__applied_file, name)
+
+        unapplied.remove(name)
+        write_strings(self.__unapplied_file, unapplied)
+
+        self.refresh_patch(bottom = head, cache_update = False, log = 'push(m)')
+
+    def push_patch(self, name):
         """Pushes a patch on the stack
         """
         unapplied = self.get_unapplied()
@@ -1032,68 +1064,61 @@ class Series(PatchSet):
         head = git.get_head()
         bottom = patch.get_bottom()
         top = patch.get_top()
+        # top != bottom always since we have a commit for each patch
 
+        if head == bottom:
+            # A fast-forward push. Just reset the backup
+            # information. No need for logging
+            patch.set_bottom(bottom, backup = True)
+            patch.set_top(top, backup = True)
+
+            git.switch(top)
+            append_string(self.__applied_file, name)
+
+            unapplied.remove(name)
+            write_strings(self.__unapplied_file, unapplied)
+            return False
+
+        # Need to create a new commit an merge in the old patch
         ex = None
         modified = False
 
-        # top != bottom always since we have a commit for each patch
-        if empty:
-            # just make an empty patch (top = bottom = HEAD). This
-            # option is useful to allow undoing already merged
-            # patches. The top is updated by refresh_patch since we
-            # need an empty commit
-            patch.set_bottom(head, backup = True)
-            patch.set_top(head, backup = True)
+        # Try the fast applying first. If this fails, fall back to the
+        # three-way merge
+        if not git.apply_diff(bottom, top):
+            # if git.apply_diff() fails, the patch requires a diff3
+            # merge and can be reported as modified
             modified = True
-        elif head == bottom:
-            # reset the backup information. No need for logging
-            patch.set_bottom(bottom, backup = True)
-            patch.set_top(top, backup = True)
 
-            git.switch(top)
-        else:
-            # new patch needs to be refreshed.
-            # The current patch is empty after merge.
-            patch.set_bottom(head, backup = True)
-            patch.set_top(head, backup = True)
-
-            # Try the fast applying first. If this fails, fall back to the
-            # three-way merge
-            if not git.apply_diff(bottom, top):
-                # if git.apply_diff() fails, the patch requires a diff3
-                # merge and can be reported as modified
-                modified = True
-
-                # merge can fail but the patch needs to be pushed
-                try:
-                    git.merge(bottom, head, top, recursive = True)
-                except git.GitException, ex:
-                    out.error('The merge failed during "push".',
-                              'Use "refresh" after fixing the conflicts or'
-                              ' revert the operation with "push --undo".')
+            # merge can fail but the patch needs to be pushed
+            try:
+                git.merge(bottom, head, top, recursive = True)
+            except git.GitException, ex:
+                out.error('The merge failed during "push".',
+                          'Use "refresh" after fixing the conflicts or'
+                          ' revert the operation with "push --undo".')
 
         append_string(self.__applied_file, name)
 
         unapplied.remove(name)
         write_strings(self.__unapplied_file, unapplied)
 
-        # head == bottom case doesn't need to refresh the patch
-        if empty or head != bottom:
-            if not ex:
-                # if the merge was OK and no conflicts, just refresh the patch
-                # The GIT cache was already updated by the merge operation
-                if modified:
-                    log = 'push(m)'
-                else:
-                    log = 'push'
-                self.refresh_patch(cache_update = False, log = log)
+        if not ex:
+            # if the merge was OK and no conflicts, just refresh the patch
+            # The GIT cache was already updated by the merge operation
+            if modified:
+                log = 'push(m)'
             else:
-                # we store the correctly merged files only for
-                # tracking the conflict history. Note that the
-                # git.merge() operations should always leave the index
-                # in a valid state (i.e. only stage 0 files)
-                self.refresh_patch(cache_update = False, log = 'push(c)')
-                raise StackException, str(ex)
+                log = 'push'
+            self.refresh_patch(bottom = head, cache_update = False, log = log)
+        else:
+            # we store the correctly merged files only for
+            # tracking the conflict history. Note that the
+            # git.merge() operations should always leave the index
+            # in a valid state (i.e. only stage 0 files)
+            self.refresh_patch(bottom = head, cache_update = False,
+                               log = 'push(c)')
+            raise StackException, str(ex)
 
         return modified