threads / patch / 7997

patchDon't use patches/<branch>/current

Subject: [StGIT PATCH] Don't use patches/<branch>/current

## tl;dr

36 messages between May 6, 2007 and Jul 1, 2007. Diffs are folded; open one to read it.

replies: 35people: 4as markdown or json

Karl Hasselström· May 6, 2007, 15:13 UTC · lore

The name of the current patch, if any, is always the last line of patches/<branch>/applied (and there is no current patch if and only if the "applied" file is empty). So use that instead, and stop having to worry about keeping the redundant "current" file up-to-date.

Signed-off-by: Karl Hasselström <kha@treskal.com>
---

This is another remove-redundant-metadata cleanup patch. Not only does it remove more code than it adds, the removed code (mostly calls to __set_current) is the kind that one easily forgets to insert in the proper places when writing new code.

 stgit/stack.py |   35 +++++++++--------------------------
 1 files changed, 9 insertions(+), 26 deletions(-)
Show changes to stgit/stack.py +9 −26
diff --git a/stgit/stack.py b/stgit/stack.py
index 2477ac6..3e9fc4f 100644
--- a/stgit/stack.py
+++ b/stgit/stack.py
@@ -295,7 +295,6 @@ class Series(StgitObject):
         self.__applied_file = os.path.join(self._dir(), 'applied')
         self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
         self.__hidden_file = os.path.join(self._dir(), 'hidden')
-        self.__current_file = os.path.join(self._dir(), 'current')
         self.__descr_file = os.path.join(self._dir(), 'description')
 
         # where this series keeps its patches
@@ -325,11 +324,6 @@ class Series(StgitObject):
         """
         return self.__name
 
-    def __set_current(self, name):
-        """Sets the topmost patch
-        """
-        self._set_field('current', name)
-
     def get_patch(self, name):
         """Return a Patch object for the given name
         """
@@ -346,11 +340,16 @@ class Series(StgitObject):
     def get_current(self):
         """Return the name of the topmost patch, or None if there is
         no such patch."""
-        name = self._get_field('current')
-        if name == '':
+        try:
+            applied = self.get_applied()
+        except StackException:
+            # No "applied" file: branch is not initialized.
+            return None
+        try:
+            return applied[-1]
+        except IndexError:
+            # No patches applied.
             return None
-        else:
-            return name
 
     def get_applied(self):
         if not os.path.isfile(self.__applied_file):
@@ -650,8 +649,6 @@ class Series(StgitObject):
                 os.remove(self.__unapplied_file)
             if os.path.exists(self.__hidden_file):
                 os.remove(self.__hidden_file)
-            if os.path.exists(self.__current_file):
-                os.remove(self.__current_file)
             if os.path.exists(self.__descr_file):
                 os.remove(self.__descr_file)
             if os.path.exists(self._dir()+'/orig-base'):
@@ -825,11 +822,8 @@ class Series(StgitObject):
             self.log_patch(patch, 'new')
 
             insert_string(self.__applied_file, patch.get_name())
-            if not self.get_current():
-                self.__set_current(name)
         else:
             append_string(self.__applied_file, patch.get_name())
-            self.__set_current(name)
             if refresh:
                 self.refresh_patch(cache_update = False, log = 'new')
 
@@ -936,8 +930,6 @@ class Series(StgitObject):
         f.writelines([line + '\n' for line in unapplied])
         f.close()
 
-        self.__set_current(name)
-
         return forwarded
 
     def merged_patches(self, names):
@@ -1019,8 +1011,6 @@ class Series(StgitObject):
         f.writelines([line + '\n' for line in unapplied])
         f.close()
 
-        self.__set_current(name)
-
         # head == bottom case doesn't need to refresh the patch
         if empty or head != bottom:
             if not ex:
@@ -1098,11 +1088,6 @@ class Series(StgitObject):
         f.writelines([line + '\n' for line in applied])
         f.close()
 
-        if applied == []:
-            self.__set_current(None)
-        else:
-            self.__set_current(applied[-1])
-
     def empty_patch(self, name):
         """Returns True if the patch is empty
         """
@@ -1144,8 +1129,6 @@ class Series(StgitObject):
             f.close()
         elif oldname in applied:
             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
-            if oldname == self.get_current():
-                self.__set_current(newname)
 
             applied[applied.index(oldname)] = newname
 
Catalin Marinas· May 15, 2007, 15:56 UTC · re: Karl Hasselström · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On 06/05/07, Karl Hasselström <kha@treskal.com> wrote:
> The name of the current patch, if any, is always the last line of
> patches/<branch>/applied (and there is no current patch if and only if
> the "applied" file is empty). So use that instead, and stop having to
> worry about keeping the redundant "current" file up-to-date.

I applied this patch. Could you also send me a patch for the bash-completion script as it uses this file?

I think the self.__current_file (same for the base file removed in a different patch) should still be available in the Series object and removed when deleting a branch, otherwise you get a "Series directory ... is not empty" exception.

Thanks.
-- 
Catalin
Peter Oberndorfer· May 15, 2007, 16:21 UTC · re: Catalin Marinas · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On Tuesday 15 May 2007 17:56, Catalin Marinas wrote:
Show 16 quoted lines
> On 06/05/07, Karl Hasselström <kha@treskal.com> wrote:
> > The name of the current patch, if any, is always the last line of
> > patches/<branch>/applied (and there is no current patch if and only if
> > the "applied" file is empty). So use that instead, and stop having to
> > worry about keeping the redundant "current" file up-to-date.
> 
> I applied this patch. Could you also send me a patch for the
> bash-completion script as it uses this file?
> 
> I think the self.__current_file (same for the base file removed in a
> different patch) should still be available in the Series object and
> removed when deleting a branch, otherwise you get a "Series directory
> ... is not empty" exception.
> 
> Thanks.
> 
Hi,
this is a bit OT,
but when i wanted to try out this changes i found that 2 unrelated patches in you repo[1] are empty.
* Store branch description in the config file 
* Make the "name" argument to "stg new" optional
Is that a problem on my side, or are they really empty?
Greetings Peter

[1] http://homepage.ntlworld.com/cmarinas/stgit.git which is mirrored at http://repo.or.cz/w/stgit.git

Catalin Marinas· May 15, 2007, 16:50 UTC · re: Peter Oberndorfer · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On 15/05/07, Peter Oberndorfer <kumbayo84@arcor.de> wrote:
> this is a bit OT,
> but when i wanted to try out this changes i found that 2 unrelated patches in you repo[1] are empty.
> * Store branch description in the config file
> * Make the "name" argument to "stg new" optional

Thanks for pointing out. They failed to apply cleanly last night and forgot to delete the empty patches created. I fixed the conflicts and added them today (I'll push them tonight).

-- 
Catalin
Karl Hasselström· May 15, 2007, 18:25 UTC · re: Catalin Marinas · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On 2007-05-15 16:56:33 +0100, Catalin Marinas wrote:
Show 10 quoted lines
> On 06/05/07, Karl Hasselström <kha@treskal.com> wrote:
>
> > The name of the current patch, if any, is always the last line of
> > patches/<branch>/applied (and there is no current patch if and
> > only if the "applied" file is empty). So use that instead, and
> > stop having to worry about keeping the redundant "current" file
> > up-to-date.
>
> I applied this patch. Could you also send me a patch for the
> bash-completion script as it uses this file?

I realized this myself yesterday or so, and patched it to not need the current, applied, and unapplied files. Are you OK with that patch, or would you like one that keeps using {,un}applied?

> I think the self.__current_file (same for the base file removed in a
> different patch) should still be available in the Series object and
> removed when deleting a branch, otherwise you get a "Series
> directory ... is not empty" exception.
Ah, very true. I'll whip up a fix.

Same question there: are you OK with a single fix for base, current, applied, and unapplied, or do you want them separate?

-- 
Karl Hasselström, kha@treskal.com
      www.treskal.com/kalle
Karl Hasselström· May 15, 2007, 19:38 UTC · re: Karl Hasselström · lore

[StGIT PATCH] Remove obsolete files when deleting a branch

Signed-off-by: Karl Hasselström <kha@treskal.com>
---
 stgit/stack.py |   10 ++++++++++
 1 files changed, 10 insertions(+), 0 deletions(-)
Show changes to stgit/stack.py +10 −0
diff --git a/stgit/stack.py b/stgit/stack.py
index 52f39a6..4ed3a73 100644
--- a/stgit/stack.py
+++ b/stgit/stack.py
@@ -660,6 +660,16 @@ class Series(StgitObject):
             if os.path.exists(self._dir()+'/orig-base'):
                 os.remove(self._dir()+'/orig-base')
 
+            # Remove obsolete files that StGIT no longer uses, but
+            # that might still be around if this is an old repository.
+            for obsolete in ([os.path.join(self._dir(), fn)
+                              for fn in ['current', 'description',
+                                         'applied', 'unapplied']]
+                             + [os.path.join(self.__base_dir,
+                                             'refs', 'bases', self.__name)]):
+                if os.path.exists(obsolete):
+                    os.remove(obsolete)
+
             if not os.listdir(self.__patch_dir):
                 os.rmdir(self.__patch_dir)
             else:
Catalin Marinas· May 15, 2007, 20:01 UTC · re: Karl Hasselström · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On 15/05/07, Karl Hasselström <kha@treskal.com> wrote:
Show 16 quoted lines
> On 2007-05-15 16:56:33 +0100, Catalin Marinas wrote:
>
> > On 06/05/07, Karl Hasselström <kha@treskal.com> wrote:
> >
> > > The name of the current patch, if any, is always the last line of
> > > patches/<branch>/applied (and there is no current patch if and
> > > only if the "applied" file is empty). So use that instead, and
> > > stop having to worry about keeping the redundant "current" file
> > > up-to-date.
> >
> > I applied this patch. Could you also send me a patch for the
> > bash-completion script as it uses this file?
>
> I realized this myself yesterday or so, and patched it to not need the
> current, applied, and unapplied files. Are you OK with that patch, or
> would you like one that keeps using {,un}applied?

What is the impact on the bash completion for calling StGIT rather than reading those files? Is it visible? If I integrate the DAG patches, there probably isn't other way anyway.

Show 9 quoted lines
> > I think the self.__current_file (same for the base file removed in a
> > different patch) should still be available in the Series object and
> > removed when deleting a branch, otherwise you get a "Series
> > directory ... is not empty" exception.
>
> Ah, very true. I'll whip up a fix.
>
> Same question there: are you OK with a single fix for base, current,
> applied, and unapplied, or do you want them separate?
Whatever is easier for you :-), I don't have any preference.

I'll push the patches I integrated in the next hour or so and you can base your changes on them.

Thanks.
-- 
Catalin
Karl Hasselström· May 16, 2007, 07:11 UTC · re: Catalin Marinas · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On 2007-05-15 21:01:43 +0100, Catalin Marinas wrote:
> What is the impact on the bash completion for calling StGIT rather
> than reading those files? Is it visible?

Yes, it's visible, but not annoying (to me anyway). The overhead is akin to the overhead we used to have when "stg help" generated the command names -- on the order of 100-200 ms, when StGIT is in the cache. The expensive part is to start stgit; the git calls are cheap. So theoretically the completion script could duplicate the logic in StGIT and avoid most of the overhead, if someone wanted it badly enough.

-- 
Karl Hasselström, kha@treskal.com
      www.treskal.com/kalle
Catalin Marinas· May 16, 2007, 12:07 UTC · re: Karl Hasselström · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On 16/05/07, Karl Hasselström <kha@treskal.com> wrote:
Show 12 quoted lines
> On 2007-05-15 21:01:43 +0100, Catalin Marinas wrote:
>
> > What is the impact on the bash completion for calling StGIT rather
> > than reading those files? Is it visible?
>
> Yes, it's visible, but not annoying (to me anyway). The overhead is
> akin to the overhead we used to have when "stg help" generated the
> command names -- on the order of 100-200 ms, when StGIT is in the
> cache. The expensive part is to start stgit; the git calls are cheap.
> So theoretically the completion script could duplicate the logic in
> StGIT and avoid most of the overhead, if someone wanted it badly
> enough.

I did a quick test of 'stg series' with the DAG patches applied, on a Linux kernel repository ('du -sh .git' is 285M) with 42 patches (only 25 applied). It constantly takes over 2 seconds to complete (compared to < 200ms without the DAG patches). The problem is that this delay will happen for bash completion as well.

It seems that most of the time is spent in git._output_lines() called from stack.read_refs() (for git-show-ref). I attach the profiling output generated by stg-prof.

-- 
Catalin


+ cross-arm
+ init-err
+ ll-printk
+ compressed-head-stack
+ armv7-uncompress
+ armv7-tlb
+ armv7-xenon
+ armv7-vfpv3
+ armv7-neon
+ armv7-thumb2-user
+ pb926-pci-io
+ mpcore-smsc911x
+ smsc911x-driver
+ realview-eb-pci-mem
+ realview-eb-pci-io
+ nommu-consistent-api
+ nommu-fault
+ nommu-page-tlb
+ nommu-context-id
+ nommu-integrator
+ nommu-realview-eb
+ nommu-kconfig
+ nommu-ptrace
+ nommu-armv6
> nommu-armv7
- per-cpu-asids
- arch-dsb-mmiowb
- t2-arch-arm-clone
- t2-function-type
- t2-exception-handling
- t2-unified-syntax
- t2-exception-handling-alternative
- t2-srs-variant
- thumb2-kernel-port
- errata-arm1136-364296
- errata-arm1136-411920
- pipt-icache-flush
- smp-writealloc
         3150 function calls (3112 primitive calls) in 2.229 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    2.229    2.229 <string>:1(?)
        1    0.000    0.000    0.000    0.000 ConfigParser.py:105(Error)
        1    0.000    0.000    0.000    0.000 ConfigParser.py:117(NoSectionError)
        1    0.000    0.000    0.000    0.000 ConfigParser.py:124(DuplicateSectionError)
        1    0.000    0.000    0.000    0.000 ConfigParser.py:131(NoOptionError)
        1    0.000    0.000    0.000    0.000 ConfigParser.py:140(InterpolationError)
        1    0.000    0.000    0.000    0.000 ConfigParser.py:148(InterpolationMissingOptionError)
        1    0.000    0.000    0.000    0.000 ConfigParser.py:161(InterpolationSyntaxError)
        1    0.000    0.000    0.000    0.000 ConfigParser.py:165(InterpolationDepthError)
        1    0.000    0.000    0.000    0.000 ConfigParser.py:176(ParsingError)
        1    0.000    0.000    0.000    0.000 ConfigParser.py:188(MissingSectionHeaderError)
        1    0.000    0.000    0.009    0.009 ConfigParser.py:202(RawConfigParser)
        1    0.000    0.000    0.000    0.000 ConfigParser.py:487(ConfigParser)
        1    0.000    0.000    0.002    0.002 ConfigParser.py:570(SafeConfigParser)
        1    0.000    0.000    0.011    0.011 ConfigParser.py:88(?)
        1    0.000    0.000    0.000    0.000 UserDict.py:19(__getitem__)
        1    0.000    0.000    0.000    0.000 UserDict.py:62(__contains__)
        1    0.000    0.000    0.000    0.000 basedir.py:2(?)
        1    0.007    0.007    0.007    0.007 basedir.py:23(__output)
        1    0.000    0.000    0.007    0.007 basedir.py:33(get)
        1    0.003    0.003    0.011    0.011 common.py:2(?)
        1    0.000    0.000    0.000    0.000 common.py:32(CmdException)
        1    0.000    0.000    0.000    0.000 common.py:37(RevParseException)
        1    0.000    0.000    0.006    0.006 config.py:125(config_setup)
        1    0.000    0.000    0.000    0.000 config.py:134(ConfigOption)
        2    0.000    0.000    0.000    0.000 config.py:137(__init__)
        1    0.000    0.000    0.000    0.000 config.py:2(?)
        1    0.000    0.000    0.000    0.000 config.py:24(GitConfigException)
        1    0.000    0.000    0.000    0.000 config.py:27(GitConfig)
        1    0.006    0.006    0.006    0.006 config.py:65(get)
        1    0.005    0.005    0.023    0.023 git.py:180(_output_one_line)
        2    2.029    1.015    2.067    1.034 git.py:192(_output_lines)
        1    0.002    0.002    0.003    0.003 git.py:2(?)
        1    0.000    0.000    0.023    0.023 git.py:288(get_head_file)
        1    0.000    0.000    0.000    0.000 git.py:30(GitException)
        1    0.000    0.000    0.000    0.000 git.py:39(Person)
        1    0.000    0.000    0.000    0.000 git.py:71(Commit)
        1    0.000    0.000    0.000    0.000 gitmergeonefile.py:2(?)
        1    0.000    0.000    0.000    0.000 gitmergeonefile.py:27(GitMergeException)
        1    0.001    0.001    2.228    2.228 main.py:197(main)
        1    0.000    0.000    0.000    0.000 main.py:32(canonical_cmd)
        1    0.000    0.000    0.015    0.015 main.py:51(__getitem__)
        1    0.000    0.000    0.000    0.000 optparse.py:1007(_create_option_list)
        1    0.000    0.000    0.001    0.001 optparse.py:1012(_populate_option_list)
        1    0.000    0.000    0.000    0.000 optparse.py:1022(_init_parsing_state)
        1    0.000    0.000    0.000    0.000 optparse.py:1036(set_usage)
        1    0.000    0.000    0.000    0.000 optparse.py:1059(get_default_values)
        1    0.000    0.000    0.000    0.000 optparse.py:1091(_get_args)
        1    0.000    0.000    0.000    0.000 optparse.py:1097(parse_args)
        1    0.000    0.000    0.000    0.000 optparse.py:1136(check_values)
        1    0.000    0.000    0.000    0.000 optparse.py:1149(_process_args)
        1    0.000    0.000    0.000    0.000 optparse.py:140(__init__)
        1    0.000    0.000    0.000    0.000 optparse.py:247(__init__)
       12    0.001    0.000    0.002    0.000 optparse.py:413(__init__)
       12    0.000    0.000    0.000    0.000 optparse.py:432(_check_opt_strings)
       12    0.000    0.000    0.000    0.000 optparse.py:441(_set_opt_strings)
       12    0.000    0.000    0.000    0.000 optparse.py:462(_set_attrs)
       12    0.000    0.000    0.000    0.000 optparse.py:480(_check_action)
       12    0.000    0.000    0.000    0.000 optparse.py:486(_check_type)
       12    0.000    0.000    0.000    0.000 optparse.py:504(_check_choice)
       12    0.000    0.000    0.000    0.000 optparse.py:517(_check_dest)
       12    0.000    0.000    0.000    0.000 optparse.py:528(_check_const)
       12    0.000    0.000    0.000    0.000 optparse.py:534(_check_nargs)
       12    0.000    0.000    0.000    0.000 optparse.py:543(_check_callback)
        1    0.000    0.000    0.000    0.000 optparse.py:662(__init__)
        1    0.000    0.000    0.000    0.000 optparse.py:748(__init__)
        1    0.000    0.000    0.000    0.000 optparse.py:759(_create_option_mappings)
        1    0.000    0.000    0.000    0.000 optparse.py:775(set_conflict_handler)
        1    0.000    0.000    0.000    0.000 optparse.py:780(set_description)
       13    0.000    0.000    0.000    0.000 optparse.py:786(_check_conflict)
       13    0.000    0.000    0.001    0.000 optparse.py:815(add_option)
        1    0.000    0.000    0.001    0.001 optparse.py:845(add_options)
        1    0.000    0.000    0.001    0.001 optparse.py:975(__init__)
        1    0.000    0.000    0.000    0.000 os.py:444(__setitem__)
        4    0.000    0.000    0.000    0.000 popen2.py:21(_cleanup)
        1    0.000    0.000    0.000    0.000 popen2.py:25(Popen3)
        4    0.064    0.016    0.064    0.016 popen2.py:31(__init__)
        1    0.000    0.000    0.000    0.000 popen2.py:7(?)
        3    0.000    0.000    0.000    0.000 popen2.py:87(wait)
        1    0.000    0.000    0.000    0.000 popen2.py:97(Popen4)
        1    0.000    0.000    0.000    0.000 posixpath.py:110(basename)
        5    0.000    0.000    0.000    0.000 posixpath.py:184(isdir)
        4    0.000    0.000    0.000    0.000 posixpath.py:197(isfile)
        8    0.000    0.000    0.000    0.000 posixpath.py:56(join)
        1    0.000    0.000    0.000    0.000 posixpath.py:74(split)
        1    0.000    0.000    2.229    2.229 profile:0(main())
        0    0.000             0.000          profile:0(profiler)
        1    0.001    0.001    2.163    2.163 series.py:111(func)
        1    0.001    0.001    0.015    0.015 series.py:2(?)
       38    0.001    0.000    0.001    0.000 series.py:88(__print_patch)
       46    0.000    0.000    0.000    0.000 sets.py:119(__iter__)
        1    0.000    0.000    0.000    0.000 sets.py:356(_update)
        1    0.000    0.000    0.000    0.000 sets.py:393(ImmutableSet)
        1    0.001    0.001    0.001    0.001 sets.py:41(?)
        1    0.000    0.000    0.000    0.000 sets.py:418(Set)
       72    0.001    0.000    0.001    0.000 sets.py:425(__init__)
       89    0.001    0.000    0.001    0.000 sets.py:515(add)
       25    0.000    0.000    0.000    0.000 sets.py:528(remove)
        1    0.000    0.000    0.000    0.000 sets.py:564(_TemporarilyImmutableSet)
        1    0.000    0.000    0.000    0.000 sets.py:83(BaseSet)
       26    0.000    0.000    0.000    0.000 sets.py:99(__len__)
        1    0.000    0.000    0.000    0.000 shutil.py:16(Error)
        1    0.000    0.000    0.000    0.000 shutil.py:5(?)
      896    0.017    0.000    0.028    0.000 sre.py:129(match)
        4    0.000    0.000    0.013    0.003 sre.py:177(compile)
      900    0.012    0.000    0.025    0.000 sre.py:216(_compile)
        5    0.000    0.000    0.001    0.000 sre_compile.py:151(_compile_charset)
        5    0.000    0.000    0.000    0.000 sre_compile.py:180(_optimize_charset)
     17/4    0.001    0.000    0.002    0.001 sre_compile.py:24(_compile)
        7    0.000    0.000    0.000    0.000 sre_compile.py:324(_simple)
        4    0.000    0.000    0.001    0.000 sre_compile.py:331(_compile_info)
       12    0.000    0.000    0.000    0.000 sre_compile.py:42(<lambda>)
        8    0.000    0.000    0.000    0.000 sre_compile.py:440(isstring)
        4    0.000    0.000    0.004    0.001 sre_compile.py:446(_code)
        4    0.000    0.000    0.013    0.003 sre_compile.py:461(compile)
       32    0.000    0.000    0.000    0.000 sre_parse.py:133(__len__)
       51    0.000    0.000    0.000    0.000 sre_parse.py:137(__getitem__)
        7    0.000    0.000    0.000    0.000 sre_parse.py:139(__setitem__)
        7    0.000    0.000    0.000    0.000 sre_parse.py:141(__getslice__)
       44    0.000    0.000    0.000    0.000 sre_parse.py:145(append)
    24/11    0.001    0.000    0.001    0.000 sre_parse.py:147(getwidth)
        4    0.000    0.000    0.000    0.000 sre_parse.py:183(__init__)
      125    0.001    0.000    0.001    0.000 sre_parse.py:187(__next)
       59    0.001    0.000    0.001    0.000 sre_parse.py:200(match)
       98    0.002    0.000    0.003    0.000 sre_parse.py:206(get)
       23    0.000    0.000    0.000    0.000 sre_parse.py:215(isident)
        4    0.000    0.000    0.001    0.000 sre_parse.py:221(isname)
        2    0.000    0.000    0.000    0.000 sre_parse.py:240(_class_escape)
        6    0.000    0.000    0.000    0.000 sre_parse.py:269(_escape)
     10/4    0.000    0.000    0.008    0.002 sre_parse.py:312(_parse_sub)
     10/4    0.003    0.000    0.008    0.002 sre_parse.py:367(_parse)
        4    0.000    0.000    0.009    0.002 sre_parse.py:614(parse)
        4    0.000    0.000    0.000    0.000 sre_parse.py:75(__init__)
        6    0.000    0.000    0.000    0.000 sre_parse.py:80(opengroup)
        6    0.000    0.000    0.000    0.000 sre_parse.py:91(closegroup)
       17    0.000    0.000    0.000    0.000 sre_parse.py:98(__init__)
        1    0.000    0.000    0.000    0.000 stack.py:112(StgitObject)
        1    0.000    0.000    0.000    0.000 stack.py:115(_set_dir)
        6    0.000    0.000    0.000    0.000 stack.py:117(_dir)
        1    0.000    0.000    0.000    0.000 stack.py:142(Patch)
        1    0.004    0.004    0.008    0.008 stack.py:2(?)
        1    0.000    0.000    0.000    0.000 stack.py:277(PatchorderCache)
        1    0.000    0.000    0.000    0.000 stack.py:280(__init__)
        1    0.000    0.000    0.000    0.000 stack.py:283(__invalidate)
       59    0.001    0.000    0.001    0.000 stack.py:286(__cache)
        2    0.000    0.000    0.000    0.000 stack.py:293(read_file)
        1    0.000    0.000    0.000    0.000 stack.py:30(StackException)
       59    0.005    0.000    0.007    0.000 stack.py:315(cmp)
        1    0.014    0.014    0.110    0.110 stack.py:327(read_refs)
        1    0.000    0.000    0.000    0.000 stack.py:33(FilterUntil)
        1    0.002    0.002    2.006    2.006 stack.py:342(unapplied_patches)
        1    0.026    0.026    0.037    0.037 stack.py:358(sort_applied_patches)
        1    0.000    0.000    0.000    0.000 stack.py:382(AppliedCache)
        1    0.000    0.000    0.000    0.000 stack.py:385(__init__)
        2    0.000    0.000    2.161    1.080 stack.py:389(get_applied)
        1    0.000    0.000    0.000    0.000 stack.py:392(get_unapplied)
        1    0.000    0.000    0.000    0.000 stack.py:417(__invalidate)
        3    0.000    0.000    0.000    0.000 stack.py:420(__cached)
        3    0.001    0.000    2.161    0.720 stack.py:422(__cache)
        1    0.000    0.000    0.000    0.000 stack.py:434(Series)
        1    0.000    0.000    0.031    0.031 stack.py:437(__init__)
        1    0.000    0.000    0.000    0.000 stack.py:479(get_branch)
        1    0.000    0.000    0.000    0.000 stack.py:497(get_current)
        2    0.000    0.000    2.161    1.080 stack.py:507(get_applied)
        1    0.000    0.000    0.000    0.000 stack.py:510(get_unapplied)
        1    0.000    0.000    0.000    0.000 stack.py:513(get_hidden)
        2    0.000    0.000    0.000    0.000 stack.py:646(is_initialised)
        8    0.000    0.000    0.000    0.000 stat.py:29(S_IFMT)
        5    0.000    0.000    0.000    0.000 stat.py:45(S_ISDIR)
        3    0.000    0.000    0.000    0.000 stat.py:54(S_ISREG)
        1    0.000    0.000    0.000    0.000 templates.py:2(?)
        1    0.000    0.000    0.000    0.000 utils.py:107(strip_prefix)
        1    0.000    0.000    0.000    0.000 utils.py:153(EditorException)
        1    0.001    0.001    0.001    0.001 utils.py:2(?)


   Ordered by: standard name

Function                                              called...
<string>:1(?)                                          main.py:197(main)(1)    2.228
ConfigParser.py:105(Error)                             --
ConfigParser.py:117(NoSectionError)                    --
ConfigParser.py:124(DuplicateSectionError)             --
ConfigParser.py:131(NoOptionError)                     --
ConfigParser.py:140(InterpolationError)                --
ConfigParser.py:148(InterpolationMissingOptionError)   --
ConfigParser.py:161(InterpolationSyntaxError)          --
ConfigParser.py:165(InterpolationDepthError)           --
ConfigParser.py:176(ParsingError)                      --
ConfigParser.py:188(MissingSectionHeaderError)         --
ConfigParser.py:202(RawConfigParser)                   sre.py:177(compile)(2)    0.013
ConfigParser.py:487(ConfigParser)                      --
ConfigParser.py:570(SafeConfigParser)                  sre.py:177(compile)(1)    0.013
ConfigParser.py:88(?)                                  ConfigParser.py:105(Error)(1)    0.000
                                                       ConfigParser.py:117(NoSectionError)(1)    0.000
                                                       ConfigParser.py:124(DuplicateSectionError)(1)    0.000
                                                       ConfigParser.py:131(NoOptionError)(1)    0.000
                                                       ConfigParser.py:140(InterpolationError)(1)    0.000
                                                       ConfigParser.py:148(InterpolationMissingOptionError)(1)    0.000
                                                       ConfigParser.py:161(InterpolationSyntaxError)(1)    0.000
                                                       ConfigParser.py:165(InterpolationDepthError)(1)    0.000
                                                       ConfigParser.py:176(ParsingError)(1)    0.000
                                                       ConfigParser.py:188(MissingSectionHeaderError)(1)    0.000
                                                       ConfigParser.py:202(RawConfigParser)(1)    0.009
                                                       ConfigParser.py:487(ConfigParser)(1)    0.000
                                                       ConfigParser.py:570(SafeConfigParser)(1)    0.002
UserDict.py:19(__getitem__)                            --
UserDict.py:62(__contains__)                           --
basedir.py:2(?)                                        --
basedir.py:23(__output)                                --
basedir.py:33(get)                                     UserDict.py:62(__contains__)(1)    0.000
                                                       basedir.py:23(__output)(1)    0.007
common.py:2(?)                                         common.py:32(CmdException)(1)    0.000
                                                       common.py:37(RevParseException)(1)    0.000
                                                       stack.py:2(?)(1)    0.008
                                                       utils.py:2(?)(1)    0.001
common.py:32(CmdException)                             --
common.py:37(RevParseException)                        --
config.py:125(config_setup)                            config.py:65(get)(1)    0.006
                                                       os.py:444(__setitem__)(1)    0.000
config.py:134(ConfigOption)                            --
config.py:137(__init__)                                --
config.py:2(?)                                         basedir.py:2(?)(1)    0.000
                                                       config.py:24(GitConfigException)(1)    0.000
                                                       config.py:27(GitConfig)(1)    0.000
                                                       config.py:134(ConfigOption)(1)    0.000
config.py:24(GitConfigException)                       --
config.py:27(GitConfig)                                --
config.py:65(get)                                      --
git.py:180(_output_one_line)                           popen2.py:31(__init__)(1)    0.064
                                                       popen2.py:87(wait)(1)    0.000
git.py:192(_output_lines)                              popen2.py:31(__init__)(2)    0.064
                                                       popen2.py:87(wait)(2)    0.000
git.py:2(?)                                            git.py:30(GitException)(1)    0.000
                                                       git.py:39(Person)(1)    0.000
                                                       git.py:71(Commit)(1)    0.000
                                                       gitmergeonefile.py:2(?)(1)    0.000
                                                       sets.py:41(?)(1)    0.001
                                                       shutil.py:5(?)(1)    0.000
git.py:288(get_head_file)                              git.py:180(_output_one_line)(1)    0.023
                                                       utils.py:107(strip_prefix)(1)    0.000
git.py:30(GitException)                                --
git.py:39(Person)                                      --
git.py:71(Commit)                                      --
gitmergeonefile.py:2(?)                                config.py:137(__init__)(2)    0.000
                                                       gitmergeonefile.py:27(GitMergeException)(1)    0.000
gitmergeonefile.py:27(GitMergeException)               --
main.py:197(main)                                      ConfigParser.py:88(?)(1)    0.011
                                                       UserDict.py:19(__getitem__)(1)    0.000
                                                       config.py:125(config_setup)(1)    0.006
                                                       main.py:32(canonical_cmd)(1)    0.000
                                                       main.py:51(__getitem__)(1)    0.015
                                                       optparse.py:975(__init__)(1)    0.001
                                                       optparse.py:1097(parse_args)(1)    0.000
                                                       posixpath.py:110(basename)(1)    0.000
                                                       series.py:111(func)(1)    2.163
                                                       stack.py:437(__init__)(1)    0.031
main.py:32(canonical_cmd)                              --
main.py:51(__getitem__)                                series.py:2(?)(1)    0.015
optparse.py:1007(_create_option_list)                  optparse.py:759(_create_option_mappings)(1)    0.000
optparse.py:1012(_populate_option_list)                optparse.py:815(add_option)(1)    0.001
                                                       optparse.py:845(add_options)(1)    0.001
optparse.py:1022(_init_parsing_state)                  --
optparse.py:1036(set_usage)                            --
optparse.py:1059(get_default_values)                   optparse.py:662(__init__)(1)    0.000
optparse.py:1091(_get_args)                            --
optparse.py:1097(parse_args)                           optparse.py:1059(get_default_values)(1)    0.000
                                                       optparse.py:1091(_get_args)(1)    0.000
                                                       optparse.py:1136(check_values)(1)    0.000
                                                       optparse.py:1149(_process_args)(1)    0.000
optparse.py:1136(check_values)                         --
optparse.py:1149(_process_args)                        --
optparse.py:140(__init__)                              --
optparse.py:247(__init__)                              optparse.py:140(__init__)(1)    0.000
optparse.py:413(__init__)                              optparse.py:432(_check_opt_strings)(12)    0.000
                                                       optparse.py:441(_set_opt_strings)(12)    0.000
                                                       optparse.py:462(_set_attrs)(12)    0.000
                                                       optparse.py:480(_check_action)(12)    0.000
                                                       optparse.py:486(_check_type)(12)    0.000
                                                       optparse.py:504(_check_choice)(12)    0.000
                                                       optparse.py:517(_check_dest)(12)    0.000
                                                       optparse.py:528(_check_const)(12)    0.000
                                                       optparse.py:534(_check_nargs)(12)    0.000
                                                       optparse.py:543(_check_callback)(12)    0.000
optparse.py:432(_check_opt_strings)                    --
optparse.py:441(_set_opt_strings)                      --
optparse.py:462(_set_attrs)                            --
optparse.py:480(_check_action)                         --
optparse.py:486(_check_type)                           --
optparse.py:504(_check_choice)                         --
optparse.py:517(_check_dest)                           --
optparse.py:528(_check_const)                          --
optparse.py:534(_check_nargs)                          --
optparse.py:543(_check_callback)                       --
optparse.py:662(__init__)                              --
optparse.py:748(__init__)                              optparse.py:775(set_conflict_handler)(1)    0.000
                                                       optparse.py:780(set_description)(1)    0.000
                                                       optparse.py:1007(_create_option_list)(1)    0.000
optparse.py:759(_create_option_mappings)               --
optparse.py:775(set_conflict_handler)                  --
optparse.py:780(set_description)                       --
optparse.py:786(_check_conflict)                       --
optparse.py:815(add_option)                            optparse.py:786(_check_conflict)(13)    0.000
optparse.py:845(add_options)                           optparse.py:815(add_option)(12)    0.001
optparse.py:975(__init__)                              optparse.py:247(__init__)(1)    0.000
                                                       optparse.py:748(__init__)(1)    0.000
                                                       optparse.py:1012(_populate_option_list)(1)    0.001
                                                       optparse.py:1022(_init_parsing_state)(1)    0.000
                                                       optparse.py:1036(set_usage)(1)    0.000
os.py:444(__setitem__)                                 --
popen2.py:21(_cleanup)                                 --
popen2.py:25(Popen3)                                   --
popen2.py:31(__init__)                                 popen2.py:21(_cleanup)(4)    0.000
popen2.py:7(?)                                         popen2.py:25(Popen3)(1)    0.000
                                                       popen2.py:97(Popen4)(1)    0.000
popen2.py:87(wait)                                     --
popen2.py:97(Popen4)                                   --
posixpath.py:110(basename)                             posixpath.py:74(split)(1)    0.000
posixpath.py:184(isdir)                                stat.py:45(S_ISDIR)(5)    0.000
posixpath.py:197(isfile)                               stat.py:54(S_ISREG)(3)    0.000
posixpath.py:56(join)                                  --
posixpath.py:74(split)                                 --
profile:0(main())                                      <string>:1(?)(1)    2.229
profile:0(profiler)                                    profile:0(main())(1)    2.229
series.py:111(func)                                    series.py:88(__print_patch)(38)    0.001
                                                       stack.py:497(get_current)(1)    0.000
                                                       stack.py:507(get_applied)(1)    2.161
                                                       stack.py:510(get_unapplied)(1)    0.000
                                                       stack.py:513(get_hidden)(1)    0.000
series.py:2(?)                                         common.py:2(?)(1)    0.011
                                                       optparse.py:413(__init__)(12)    0.002
series.py:88(__print_patch)                            --
sets.py:119(__iter__)                                  --
sets.py:356(_update)                                   --
sets.py:393(ImmutableSet)                              --
sets.py:41(?)                                          sets.py:83(BaseSet)(1)    0.000
                                                       sets.py:393(ImmutableSet)(1)    0.000
                                                       sets.py:418(Set)(1)    0.000
                                                       sets.py:564(_TemporarilyImmutableSet)(1)    0.000
sets.py:418(Set)                                       --
sets.py:425(__init__)                                  sets.py:356(_update)(1)    0.000
sets.py:515(add)                                       --
sets.py:528(remove)                                    --
sets.py:564(_TemporarilyImmutableSet)                  --
sets.py:83(BaseSet)                                    --
sets.py:99(__len__)                                    --
shutil.py:16(Error)                                    --
shutil.py:5(?)                                         shutil.py:16(Error)(1)    0.000
sre.py:129(match)                                      sre.py:216(_compile)(896)    0.025
sre.py:177(compile)                                    sre.py:216(_compile)(4)    0.025
sre.py:216(_compile)                                   sre_compile.py:440(isstring)(4)    0.000
                                                       sre_compile.py:461(compile)(4)    0.013
sre_compile.py:151(_compile_charset)                   sre_compile.py:42(<lambda>)(6)    0.000
                                                       sre_compile.py:180(_optimize_charset)(5)    0.000
sre_compile.py:180(_optimize_charset)                  sre_compile.py:42(<lambda>)(6)    0.000
sre_compile.py:24(_compile)                            sre_compile.py:24(_compile)(13)    0.002
                                                       sre_compile.py:151(_compile_charset)(5)    0.001
                                                       sre_compile.py:324(_simple)(7)    0.000
                                                       sre_parse.py:137(__getitem__)(27)    0.000
sre_compile.py:324(_simple)                            sre_parse.py:137(__getitem__)(7)    0.000
                                                       sre_parse.py:147(getwidth)(7)    0.001
sre_compile.py:331(_compile_info)                      sre_parse.py:133(__len__)(4)    0.000
                                                       sre_parse.py:137(__getitem__)(3)    0.000
                                                       sre_parse.py:147(getwidth)(4)    0.001
sre_compile.py:42(<lambda>)                            --
sre_compile.py:440(isstring)                           --
sre_compile.py:446(_code)                              sre_compile.py:24(_compile)(4)    0.002
                                                       sre_compile.py:331(_compile_info)(4)    0.001
sre_compile.py:461(compile)                            sre_compile.py:440(isstring)(4)    0.000
                                                       sre_compile.py:446(_code)(4)    0.004
                                                       sre_parse.py:614(parse)(4)    0.009
sre_parse.py:133(__len__)                              --
sre_parse.py:137(__getitem__)                          --
sre_parse.py:139(__setitem__)                          --
sre_parse.py:141(__getslice__)                         sre_parse.py:98(__init__)(7)    0.000
sre_parse.py:145(append)                               --
sre_parse.py:147(getwidth)                             sre_parse.py:147(getwidth)(13)    0.001
sre_parse.py:183(__init__)                             sre_parse.py:187(__next)(4)    0.001
sre_parse.py:187(__next)                               --
sre_parse.py:200(match)                                sre_parse.py:187(__next)(23)    0.001
sre_parse.py:206(get)                                  sre_parse.py:187(__next)(98)    0.001
sre_parse.py:215(isident)                              --
sre_parse.py:221(isname)                               sre_parse.py:215(isident)(23)    0.000
sre_parse.py:240(_class_escape)                        --
sre_parse.py:269(_escape)                              --
sre_parse.py:312(_parse_sub)                           sre_parse.py:200(match)(16)    0.001
                                                       sre_parse.py:367(_parse)(10)    0.008
sre_parse.py:367(_parse)                               sre_parse.py:80(opengroup)(6)    0.000
                                                       sre_parse.py:91(closegroup)(6)    0.000
                                                       sre_parse.py:98(__init__)(10)    0.000
                                                       sre_parse.py:133(__len__)(28)    0.000
                                                       sre_parse.py:137(__getitem__)(14)    0.000
                                                       sre_parse.py:139(__setitem__)(7)    0.000
                                                       sre_parse.py:141(__getslice__)(7)    0.000
                                                       sre_parse.py:145(append)(44)    0.000
                                                       sre_parse.py:200(match)(43)    0.001
                                                       sre_parse.py:206(get)(94)    0.003
                                                       sre_parse.py:221(isname)(4)    0.001
                                                       sre_parse.py:240(_class_escape)(2)    0.000
                                                       sre_parse.py:269(_escape)(6)    0.000
                                                       sre_parse.py:312(_parse_sub)(6)    0.008
sre_parse.py:614(parse)                                sre_parse.py:75(__init__)(4)    0.000
                                                       sre_parse.py:183(__init__)(4)    0.000
                                                       sre_parse.py:206(get)(4)    0.003
                                                       sre_parse.py:312(_parse_sub)(4)    0.008
sre_parse.py:75(__init__)                              --
sre_parse.py:80(opengroup)                             --
sre_parse.py:91(closegroup)                            --
sre_parse.py:98(__init__)                              --
stack.py:112(StgitObject)                              --
stack.py:115(_set_dir)                                 --
stack.py:117(_dir)                                     --
stack.py:142(Patch)                                    --
stack.py:2(?)                                          git.py:2(?)(1)    0.003
                                                       popen2.py:7(?)(1)    0.000
                                                       stack.py:30(StackException)(1)    0.000
                                                       stack.py:33(FilterUntil)(1)    0.000
                                                       stack.py:112(StgitObject)(1)    0.000
                                                       stack.py:142(Patch)(1)    0.000
                                                       stack.py:277(PatchorderCache)(1)    0.000
                                                       stack.py:382(AppliedCache)(1)    0.000
                                                       stack.py:434(Series)(1)    0.000
                                                       templates.py:2(?)(1)    0.000
stack.py:277(PatchorderCache)                          --
stack.py:280(__init__)                                 stack.py:283(__invalidate)(1)    0.000
stack.py:283(__invalidate)                             --
stack.py:286(__cache)                                  posixpath.py:56(join)(3)    0.000
                                                       posixpath.py:197(isfile)(3)    0.000
                                                       stack.py:117(_dir)(3)    0.000
                                                       stack.py:293(read_file)(2)    0.000
stack.py:293(read_file)                                --
stack.py:30(StackException)                            --
stack.py:315(cmp)                                      stack.py:286(__cache)(59)    0.001
stack.py:327(read_refs)                                git.py:192(_output_lines)(1)    2.067
                                                       sre.py:129(match)(896)    0.028
                                                       sre.py:177(compile)(1)    0.013
stack.py:33(FilterUntil)                               --
stack.py:342(unapplied_patches)                        git.py:192(_output_lines)(1)    2.067
                                                       sets.py:119(__iter__)(19)    0.000
                                                       sets.py:425(__init__)(46)    0.001
                                                       sets.py:515(add)(64)    0.001
stack.py:358(sort_applied_patches)                     popen2.py:31(__init__)(1)    0.064
                                                       sets.py:99(__len__)(26)    0.000
                                                       sets.py:119(__iter__)(25)    0.000
                                                       sets.py:425(__init__)(26)    0.001
                                                       sets.py:515(add)(25)    0.001
                                                       sets.py:528(remove)(25)    0.000
stack.py:382(AppliedCache)                             --
stack.py:385(__init__)                                 stack.py:280(__init__)(1)    0.000
                                                       stack.py:417(__invalidate)(1)    0.000
stack.py:389(get_applied)                              stack.py:422(__cache)(2)    2.161
stack.py:392(get_unapplied)                            stack.py:422(__cache)(1)    2.161
stack.py:417(__invalidate)                             --
stack.py:420(__cached)                                 --
stack.py:422(__cache)                                  sets.py:119(__iter__)(2)    0.000
                                                       stack.py:315(cmp)(59)    0.007
                                                       stack.py:327(read_refs)(1)    0.110
                                                       stack.py:342(unapplied_patches)(1)    2.006
                                                       stack.py:358(sort_applied_patches)(1)    0.037
                                                       stack.py:420(__cached)(3)    0.000
                                                       stack.py:479(get_branch)(1)    0.000
stack.py:434(Series)                                   --
stack.py:437(__init__)                                 basedir.py:33(get)(1)    0.007
                                                       git.py:288(get_head_file)(1)    0.023
                                                       posixpath.py:56(join)(5)    0.000
                                                       posixpath.py:184(isdir)(3)    0.000
                                                       stack.py:115(_set_dir)(1)    0.000
                                                       stack.py:117(_dir)(3)    0.000
                                                       stack.py:385(__init__)(1)    0.000
                                                       stack.py:646(is_initialised)(2)    0.000
stack.py:479(get_branch)                               --
stack.py:497(get_current)                              stack.py:507(get_applied)(1)    2.161
stack.py:507(get_applied)                              stack.py:389(get_applied)(2)    2.161
stack.py:510(get_unapplied)                            stack.py:392(get_unapplied)(1)    0.000
stack.py:513(get_hidden)                               posixpath.py:197(isfile)(1)    0.000
stack.py:646(is_initialised)                           posixpath.py:184(isdir)(2)    0.000
stat.py:29(S_IFMT)                                     --
stat.py:45(S_ISDIR)                                    stat.py:29(S_IFMT)(5)    0.000
stat.py:54(S_ISREG)                                    stat.py:29(S_IFMT)(3)    0.000
templates.py:2(?)                                      --
utils.py:107(strip_prefix)                             --
utils.py:153(EditorException)                          --
utils.py:2(?)                                          config.py:2(?)(1)    0.000
                                                       utils.py:153(EditorException)(1)    0.000
Karl Hasselström· May 16, 2007, 19:40 UTC · re: Catalin Marinas · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On 2007-05-16 13:07:14 +0100, Catalin Marinas wrote:
Show 5 quoted lines
> I did a quick test of 'stg series' with the DAG patches applied, on
> a Linux kernel repository ('du -sh .git' is 285M) with 42 patches
> (only 25 applied). It constantly takes over 2 seconds to complete
> (compared to < 200ms without the DAG patches). The problem is that
> this delay will happen for bash completion as well.

This is more than ten times as expensive as in my measurement. Curious. And the algorithm is designed so that it shouldn't take time proportional to the repository size, just proportional to the number of patches.

There are three git calls involved:
  * List the references. There can't be more than a few hundred of
    them.
  * rev-list all patches, subtracting everything that's reachable from
    the branch head. This set of commits should not be much larger
    than the number of unapplied patches.
  * rev-list the branch head, but stop walking as soon as all applied
    patches have been seen. This set of commits should not be much
    larger than the number of applied patches.
None of the calls should be expensive.
-- 
Karl Hasselström, kha@treskal.com
      www.treskal.com/kalle
Karl Hasselström· May 16, 2007, 20:40 UTC · re: Karl Hasselström · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On 2007-05-16 21:40:02 +0200, Karl Hasselström wrote:
Show 13 quoted lines
> On 2007-05-16 13:07:14 +0100, Catalin Marinas wrote:
>
> > I did a quick test of 'stg series' with the DAG patches applied,
> > on a Linux kernel repository ('du -sh .git' is 285M) with 42
> > patches (only 25 applied). It constantly takes over 2 seconds to
> > complete (compared to < 200ms without the DAG patches). The
> > problem is that this delay will happen for bash completion as
> > well.
>
> This is more than ten times as expensive as in my measurement.
> Curious. And the algorithm is designed so that it shouldn't take
> time proportional to the repository size, just proportional to the
> number of patches.

I set up a kernel repository with 100 applied and 100 unapplied patches:

  $ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
  $ cd linux-2.6/
  $ stg init
  $ for ((i=0;i<200;i++)); do stg new p$(printf '%03d' $i) -m "Patch $i"; done
  $ stg goto p099
Then repeatedly:
  $ time stg series >/dev/null
This gives times such as
  real    0m0.247s
  user    0m0.156s
  sys     0m0.060s
  real    0m0.147s
  user    0m0.088s
  sys     0m0.036s
  real    0m0.153s
  user    0m0.088s
  sys     0m0.032s
  real    0m0.144s
  user    0m0.104s
  sys     0m0.024s

Tab completion also feels like it takes 0.1-0.2 seconds -- which it should, since it's implemented with series, applied, and unapplied, allow which do the same amount of work.

But my kernel repository is _much_ smaller than yours:
  $ du -sh .git
  183M    .git
Do you perchance have a bunch of loose objects in there?
-- 
Karl Hasselström, kha@treskal.com
      www.treskal.com/kalle
Catalin Marinas· May 17, 2007, 12:43 UTC · re: Karl Hasselström · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On 16/05/07, Karl Hasselström <kha@treskal.com> wrote:
> I set up a kernel repository with 100 applied and 100 unapplied
> patches:
[...]
> Then repeatedly:
>
>   $ time stg series >/dev/null

I ran 'git repack -a -d' and 'git prune'. There are no other objects apart from the generated pack:

$ du -sh .git 211M .git

And then repeatedly 'time stg series > /dev/null':

real 0m1.638s user 0m1.422s sys 0m0.088s

real 0m2.542s user 0m1.436s sys 0m0.078s

real 0m2.916s user 0m1.424s sys 0m0.083s

real 0m2.940s user 0m1.425s sys 0m0.081s

real 0m1.614s user 0m1.421s sys 0m0.081s

real 0m1.587s user 0m1.423s sys 0m0.081s

real 0m2.653s user 0m1.427s sys 0m0.075s

Show 6 quoted lines
> But my kernel repository is _much_ smaller than yours:
>
>   $ du -sh .git
>   183M    .git
>
> Do you perchance have a bunch of loose objects in there?

It got smaller after repacking but it is still bigger than yours. Maybe the reason is that I have 14 branches with various patches, some of them just for historical reasons but going back to 2.6.12. There are also several commits generated for the patch logs.

The CPU is a P4 at 2.5GHz and the 'stg series' operation seems to be CPU bound rather than IO. I'm also using Python 2.3 on this PC and for this reason I changed 2 generator constructs (x for x in ...) with list comprehension (see the attached patch).

-- 
Catalin


commit f7cc1f7fa6b2ee4f42e812970760ba5db5d2cafc
Author: Catalin Marinas <catalin.marinas@gmail.com>
Date:   Wed May 16 13:08:45 2007 +0100

    refresh	b5003aa7bb5ec384e1dbe2887c07885a791384c2

diff --git a/stgit/stack.py b/stgit/stack.py
index 13eb692..0ef26b8 100644
--- a/stgit/stack.py
+++ b/stgit/stack.py
@@ -349,8 +349,8 @@ def unapplied_patches(ref2hash):
     unapplied = Set()
     for line in git._output_lines(
         'git-rev-list --stdin',
-        ('%s%s\n' % (['', '^'][ref == None], sha1)
-         for ref, sha1 in ref2hash.iteritems())):
+        ['%s%s\n' % (['', '^'][ref == None], sha1)
+         for ref, sha1 in ref2hash.iteritems()]):
         for ref in hash2refs.get(line.strip(), []):
             unapplied.add(ref)
     return unapplied
@@ -364,7 +364,7 @@ def sort_applied_patches(ref2hash):
         if r != None:
             hash2refs.setdefault(h, Set()).add(r)
 
-    missing = Set(ref for ref in ref2hash.iterkeys() if ref != None)
+    missing = Set([ref for ref in ref2hash.iterkeys() if ref != None])
     if not missing:
         return []
     applied = []
Karl Hasselström· May 17, 2007, 14:57 UTC · re: Catalin Marinas · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On 2007-05-17 13:43:35 +0100, Catalin Marinas wrote:
Show 7 quoted lines
> I ran 'git repack -a -d' and 'git prune'. There are no other objects
> apart from the generated pack:
>
> $ du -sh .git
> 211M    .git
>
> And then repeatedly 'time stg series > /dev/null':
Hmm, it seems there is a problem then. :-(
> It got smaller after repacking but it is still bigger than yours.
> Maybe the reason is that I have 14 branches with various patches,
> some of them just for historical reasons but going back to 2.6.12.
> There are also several commits generated for the patch logs.

OK. That shouldn't matter, though, since that extra history shouldn't be examined anyway.

> The CPU is a P4 at 2.5GHz and the 'stg series' operation seems to be
> CPU bound rather than IO. I'm also using Python 2.3 on this PC and
> for this reason I changed 2 generator constructs (x for x in ...)
> with list comprehension (see the attached patch).
I don't think that's the problem, since those lists are both small.

The only possibility I can think of that might explain this is that some of your unapplied patches are attached to a place in the commit DAG that's far away from the branch head (e.g. you have rebased to some entirely different place since you last had them applied), so that "git-rev-list patch ^branch" outputs a large part of the commit DAG.

Could you put counters in unapplied_patches() and sort_applied_patches() to see how many lines each of them reads from git-rev-list? The expected number (if it had taken just a little time, like it did for me) is a small constant times the number of patches in both cases.

-- 
Karl Hasselström, kha@treskal.com
      www.treskal.com/kalle
Catalin Marinas· May 17, 2007, 20:51 UTC · re: Karl Hasselström · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On 17/05/07, Karl Hasselström <kha@treskal.com> wrote:
Show 6 quoted lines
> The only possibility I can think of that might explain this is that
> some of your unapplied patches are attached to a place in the commit
> DAG that's far away from the branch head (e.g. you have rebased to
> some entirely different place since you last had them applied), so
> that "git-rev-list patch ^branch" outputs a large part of the commit
> DAG.

That's probably the case. I have patches that I haven't rebased for months but I keep them in case they might be needed in the future. That's the reason for the hide/unhide commands. Anyway, I'm not yet prepared to give up my current workflow.

I haven't tried to understand your patch yet but the unapplied patches will never be in a linear DAG similar to the applied patches. Because of this, we need to keep their order in a file anyway and we might not need to run git-rev-list (BTW, how do you preserve the unapplied patches order with the DAG implementation?).

Show 5 quoted lines
> Could you put counters in unapplied_patches() and
> sort_applied_patches() to see how many lines each of them reads from
> git-rev-list? The expected number (if it had taken just a little time,
> like it did for me) is a small constant times the number of patches in
> both cases.
I'll do this tomorrow to confirm but that's probably the cause of the slow-down.
-- 
Catalin
Karl Hasselström· May 18, 2007, 06:30 UTC · re: Catalin Marinas · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On 2007-05-17 21:51:19 +0100, Catalin Marinas wrote:
Show 11 quoted lines
> On 17/05/07, Karl Hasselström <kha@treskal.com> wrote:
>
> > The only possibility I can think of that might explain this is
> > that some of your unapplied patches are attached to a place in the
> > commit DAG that's far away from the branch head (e.g. you have
> > rebased to some entirely different place since you last had them
> > applied), so that "git-rev-list patch ^branch" outputs a large
> > part of the commit DAG.
>
> That's probably the case. I have patches that I haven't rebased for
> months but I keep them in case they might be needed in the future.

That's probably it, then. I'll need to come up with a clever scheme to make that case cheap again.

> That's the reason for the hide/unhide commands.
Ah.
> Anyway, I'm not yet prepared to give up my current workflow.

I didn't intend to make that workflow painful, so don't despair yet -- there might be a way to make it work.

> I haven't tried to understand your patch yet but the unapplied
> patches will never be in a linear DAG similar to the applied
> patches.
I know.
> Because of this, we need to keep their order in a file anyway and we
> might not need to run git-rev-list (BTW, how do you preserve the
> unapplied patches order with the DAG implementation?).

The order of _all_ patches is kept in the "patchorder" file. This file is consulted only when the relative order of two unapplied patches need to be established. (Applied patches are sorted by the DAG, and applied patches always precede unapplied patches.)

I would like to avoid keeping a record of known-to-be-unapplied patches; while this is nowhere near as important as not keeping a record of applied patches, it would still provide an opening for the user to confuse StGIT.

But I believe I have a cunning plan ...
Show 8 quoted lines
> > Could you put counters in unapplied_patches() and
> > sort_applied_patches() to see how many lines each of them reads
> > from git-rev-list? The expected number (if it had taken just a
> > little time, like it did for me) is a small constant times the
> > number of patches in both cases.
>
> I'll do this tomorrow to confirm but that's probably the cause of
> the slow-down.
Thanks for hanging in there.
-- 
Karl Hasselström, kha@treskal.com
      www.treskal.com/kalle
Karl Hasselström· Jun 10, 2007, 09:54 UTC · re: Karl Hasselström · lore

[StGIT PATCH 0/6] New and improved DAG appliedness series

This took a while, but here it is. (Actually, I finished this about a week ago, but had no Internet connection so I couldn't send it.)

The series starts with basically the same DAG appliedness test as before, with the same known performance bug. Later on in the series, the mechanism is changed to one that doesn't have the bug. I kept the intermediate, slow state because the changesets read better that way, and because the new mechanism is more complicated than the old so it might be useful to be able to compare their output in case some bug turns up further down the road.

To test the performance, I used a script (which I've unfortunately misplaced) that (in the kernel repository) reset to one point 10000 commits in the past and one 5000 commits in the past, pushed a few patches at each spot, and then created 100 applied and 100 unapplied patches on top of upstream HEAD. This triggers the performance bug with the first algorithm since we have unapplied commits very far from HEAD.

Both algorithms are documented in the patches that introduce them.
-- 
Karl Hasselström, kha@treskal.com
      www.treskal.com/kalle
Karl Hasselström· Jun 10, 2007, 09:54 UTC · re: Karl Hasselström · lore

[StGIT PATCH 1/6] Verify patch status during the test

Signed-off-by: Karl Hasselström <kha@treskal.com>
---
 t/t1200-push-modified.sh |   55 +++++++++++++++++++++++++++-------------------
 1 files changed, 32 insertions(+), 23 deletions(-)
Show changes to t/t1200-push-modified.sh +32 −23
diff --git a/t/t1200-push-modified.sh b/t/t1200-push-modified.sh
index 6769667..aa4ffd0 100755
--- a/t/t1200-push-modified.sh
+++ b/t/t1200-push-modified.sh
@@ -19,23 +19,26 @@ specify --merged, then rollback and retry with the correct flag.'
 test_create_repo foo
 
 test_expect_success \
-    'Clone tree and setup changes' \
-    "stg clone foo bar &&
-     (cd bar && stg new p1 -m p1
-      printf 'a\nc\n' > file && stg add file && stg refresh &&
-      stg new p2 -m p2
-      printf 'a\nb\nc\n' > file && stg refresh
-     )
-"
+    'Clone tree and setup changes' '
+    stg clone foo bar &&
+    (
+        cd bar && stg new p1 -m p1
+        printf "a\nc\n" > file && stg add file && stg refresh &&
+        stg new p2 -m p2 &&
+        printf "a\nb\nc\n" > file && stg refresh &&
+        [ "$(echo $(stg applied))" = "p1 p2" ] &&
+        [ "$(echo $(stg unapplied))" = "" ]
+    )
+'
 
 test_expect_success \
-    'Port those patches to orig tree' \
-    '(cd foo &&
-      GIT_DIR=../bar/.git git-format-patch --stdout \
-          $(cd ../bar && stg id base@master)..HEAD |
-      git-am -3 -k
-     )
-    '
+    'Port those patches to orig tree' '
+    (
+        cd foo &&
+        GIT_DIR=../bar/.git git-format-patch --stdout \
+          $(cd ../bar && stg id base@master)..HEAD | git-am -3 -k
+    )
+'
 
 test_expect_success \
     'Pull to sync with parent, preparing for the problem' \
@@ -51,15 +54,21 @@ test_expect_failure \
 "
 
 test_expect_success \
-    'Rollback the push' \
-    "(cd bar && stg push --undo
-     )
-"
+    'Rollback the push' '
+    (
+        cd bar && stg push --undo &&
+        [ "$(echo $(stg applied))" = "" ] &&
+        [ "$(echo $(stg unapplied))" = "p1 p2" ]
+    )
+'
 
 test_expect_success \
-    'Push those patches while checking they were merged upstream' \
-    "(cd bar && stg push --merged --all
-     )
-"
+    'Push those patches while checking they were merged upstream' '
+    (
+        cd bar && stg push --merged --all
+        [ "$(echo $(stg applied))" = "p1 p2" ] &&
+        [ "$(echo $(stg unapplied))" = "" ]
+    )
+'
 
 test_done
Karl Hasselström· Jun 10, 2007, 09:55 UTC · re: Karl Hasselström · lore

[StGIT PATCH 2/6] Make use of the get_patch() utility function

We already had it, but no one was using it
Signed-off-by: Karl Hasselström <kha@treskal.com>
---
 stgit/stack.py |   27 +++++++++++++--------------
 1 files changed, 13 insertions(+), 14 deletions(-)
Show changes to stgit/stack.py +13 −14
diff --git a/stgit/stack.py b/stgit/stack.py
index 7a06458..0a486bd 100644
--- a/stgit/stack.py
+++ b/stgit/stack.py
@@ -404,7 +404,7 @@ class Series(StgitObject):
         crt = self.get_current()
         if not crt:
             return None
-        return Patch(crt, self.__patch_dir, self.__refs_dir)
+        return self.get_patch(crt)
 
     def get_current(self):
         """Return the name of the topmost patch, or None if there is
@@ -678,7 +678,7 @@ class Series(StgitObject):
                 raise StackException, \
                       'Cannot delete: the series still contains patches'
             for p in patches:
-                Patch(p, self.__patch_dir, self.__refs_dir).delete()
+                self.get_patch(p).delete()
 
             # remove the trash directory
             for fname in os.listdir(self.__trash_dir):
@@ -732,7 +732,7 @@ class Series(StgitObject):
         if not name:
             raise StackException, 'No patches applied'
 
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        patch = self.get_patch(name)
 
         descr = patch.get_description()
         if not (message or descr):
@@ -798,7 +798,7 @@ class Series(StgitObject):
         name = self.get_current()
         assert(name)
 
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        patch = self.get_patch(name)
         old_bottom = patch.get_old_bottom()
         old_top = patch.get_old_top()
 
@@ -839,7 +839,7 @@ class Series(StgitObject):
         if name == None:
             name = make_patch_name(descr, self.patch_exists)
 
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        patch = self.get_patch(name)
         patch.create()
 
         if bottom:
@@ -919,7 +919,7 @@ class Series(StgitObject):
         for name in names:
             assert(name in unapplied)
 
-            patch = Patch(name, self.__patch_dir, self.__refs_dir)
+            patch = self.get_patch(name)
 
             head = top
             bottom = patch.get_bottom()
@@ -988,8 +988,7 @@ class Series(StgitObject):
         patches detected to have been applied. The state of the tree
         is restored to the original one
         """
-        patches = [Patch(name, self.__patch_dir, self.__refs_dir)
-                   for name in names]
+        patches = [self.get_patch(name) for name in names]
         patches.reverse()
 
         merged = []
@@ -1008,7 +1007,7 @@ class Series(StgitObject):
         unapplied = self.get_unapplied()
         assert(name in unapplied)
 
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        patch = self.get_patch(name)
 
         head = git.get_head()
         bottom = patch.get_bottom()
@@ -1084,7 +1083,7 @@ class Series(StgitObject):
         name = self.get_current()
         assert(name)
 
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        patch = self.get_patch(name)
         old_bottom = patch.get_old_bottom()
         old_top = patch.get_old_top()
 
@@ -1110,7 +1109,7 @@ class Series(StgitObject):
         applied.reverse()
         assert(name in applied)
 
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        patch = self.get_patch(name)
 
         if git.get_head_file() == self.get_branch():
             if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
@@ -1142,7 +1141,7 @@ class Series(StgitObject):
         """Returns True if the patch is empty
         """
         self.__patch_name_valid(name)
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        patch = self.get_patch(name)
         bottom = patch.get_bottom()
         top = patch.get_top()
 
@@ -1171,14 +1170,14 @@ class Series(StgitObject):
             self.hide_patch(newname)
 
         if oldname in unapplied:
-            Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
+            self.get_patch(oldname).rename(newname)
             unapplied[unapplied.index(oldname)] = newname
 
             f = file(self.__unapplied_file, 'w+')
             f.writelines([line + '\n' for line in unapplied])
             f.close()
         elif oldname in applied:
-            Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
+            self.get_patch(oldname).rename(newname)
 
             applied[applied.index(oldname)] = newname
 
Karl Hasselström· Jun 10, 2007, 09:55 UTC · re: Karl Hasselström · lore

[StGIT PATCH 3/6] Compute patch appliedness from commit DAG

Don't rely on cached metadata in the "applied" and "unapplied" files to tell which patches are applied. Instead, consider the patches reachable from the branch head to be applied, and the rest unapplied.

The order of the applied patches is also taken from the DAG, but we can't do that for the unapplied patches. So the patch order is saved to a file whenever it changes, and that file is consulted whenever we need to compute the order of the unapplied patches.

The point of this excercise is to let users do things such as "git reset" without confusing stgit. This gives incrased flexibility to power users, and increased safety to other users. The advantages come from the removal of redundant metadata: it is no longer possible for StGIT's appliedness status to get out of sync with the underlying git commit DAG.

This is how the appliedness and order is computed:
  * First, a single call to git-show-ref gives the hashes of all
    patches and the branch head.
  * Then, "git-rev-list patch1 patch2 patch3 ^branch" lists a small
    set of hashes that contains all the unapplied patches and none of
    the applied patches.
  * Last, "git-rev-list head" lists all commits in the branch. The
    applied patches are listed in the correct order.

This is efficient because none of the two rev-list calls need to look at more than a small part of the DAG. The first call returns a small set of commits, and the last call is abandoned before it has time to look far back in the DAG.

Signed-off-by: Karl Hasselström <kha@treskal.com>
---
 stgit/commands/commit.py  |    8 -
 stgit/commands/float.py   |    2 
 stgit/commands/imprt.py   |    2 
 stgit/commands/refresh.py |    2 
 stgit/commands/sync.py    |    2 
 stgit/git.py              |    5 +
 stgit/stack.py            |  333 +++++++++++++++++++++++++++++----------------
 t/t4000-upgrade.sh        |    6 +
 8 files changed, 227 insertions(+), 133 deletions(-)
Show changes to 8 files +227 −133

stgit/commands/commit.py, stgit/commands/float.py, stgit/commands/imprt.py, stgit/commands/refresh.py, stgit/commands/sync.py, stgit/git.py, stgit/stack.py, t/t4000-upgrade.sh

diff --git a/stgit/commands/commit.py b/stgit/commands/commit.py
index 2b8d7ce..5450112 100644
--- a/stgit/commands/commit.py
+++ b/stgit/commands/commit.py
@@ -52,14 +52,8 @@ def func(parser, options, args):
     if crt_series.get_protected():
         raise CmdException, 'This branch is protected.  Commit is not permitted'
 
-    crt_head = git.get_head()
-
     out.start('Committing %d patches' % len(applied))
-
-    crt_series.pop_patch(applied[0])
-    git.switch(crt_head)
-
     for patch in applied:
-        crt_series.delete_patch(patch)
+        crt_series.delete_patch_data(patch)
 
     out.done()
diff --git a/stgit/commands/float.py b/stgit/commands/float.py
index 0e32f6b..8ba76d5 100644
--- a/stgit/commands/float.py
+++ b/stgit/commands/float.py
@@ -48,7 +48,7 @@ def func(parser, options, args):
     check_head_top_equal()
 
     unapplied = crt_series.get_unapplied()
-    applied = crt_series.get_applied()
+    applied = list(crt_series.get_applied()) # a copy, since we'll modify it
     all = unapplied + applied
 
     if options.series:
diff --git a/stgit/commands/imprt.py b/stgit/commands/imprt.py
index 0089a8b..b7a34fa 100644
--- a/stgit/commands/imprt.py
+++ b/stgit/commands/imprt.py
@@ -294,7 +294,7 @@ def __create_patch(filename, message, author_name, author_email,
         git.apply_patch(diff = diff, base = git_id(options.base))
     else:
         git.apply_patch(diff = diff)
-    crt_series.refresh_patch(edit = options.edit,
+    crt_series.refresh_patch(patch, edit = options.edit,
                              show_patch = options.showpatch)
     out.done()
 
diff --git a/stgit/commands/refresh.py b/stgit/commands/refresh.py
index 77dcbda..d560951 100644
--- a/stgit/commands/refresh.py
+++ b/stgit/commands/refresh.py
@@ -131,7 +131,7 @@ def func(parser, options, args):
 
         if autoresolved == 'yes':
             resolved_all()
-        crt_series.refresh_patch(files = args,
+        crt_series.refresh_patch(patch, files = args,
                                  message = options.message,
                                  edit = options.edit,
                                  show_patch = options.showpatch,
diff --git a/stgit/commands/sync.py b/stgit/commands/sync.py
index 8359061..a42eeac 100644
--- a/stgit/commands/sync.py
+++ b/stgit/commands/sync.py
@@ -161,7 +161,7 @@ def func(parser, options, args):
         if git.local_changes(verbose = False):
             # index (cache) already updated by the git merge. The
             # backup information was already reset above
-            crt_series.refresh_patch(cache_update = False, backup = False,
+            crt_series.refresh_patch(p, cache_update = False, backup = False,
                                      log = 'sync')
             out.done('updated')
         else:
diff --git a/stgit/git.py b/stgit/git.py
index 845c712..703425b 100644
--- a/stgit/git.py
+++ b/stgit/git.py
@@ -189,8 +189,11 @@ def _output_one_line(cmd, file_desc = None):
                                                 p.childerr.read().strip())
     return output
 
-def _output_lines(cmd):
+def _output_lines(cmd, input = []):
     p=popen2.Popen3(cmd, True)
+    for line in input:
+        p.tochild.write(line)
+    p.tochild.close()
     lines = p.fromchild.readlines()
     if p.wait():
         raise GitException, '%s failed (%s)' % (str(cmd),
diff --git a/stgit/stack.py b/stgit/stack.py
index 0a486bd..e622f68 100644
--- a/stgit/stack.py
+++ b/stgit/stack.py
@@ -18,12 +18,13 @@ along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
-import sys, os, re
+import sys, os, popen2, re
 
 from stgit.utils import *
 from stgit import git, basedir, templates
 from stgit.config import config
 from shutil import copyfile
+from sets import Set
 
 
 # stack exception class
@@ -274,7 +275,7 @@ class Patch(StgitObject):
         self.__update_log_ref(value)
 
 # The current StGIT metadata format version.
-FORMAT_VERSION = 2
+FORMAT_VERSION = 3
 
 def format_version_key(branch):
     return 'branch.%s.stgitformatversion' % branch
@@ -343,11 +344,173 @@ def update_to_current_format_version(branch, git_dir):
         rm(os.path.join(git_dir, 'refs', 'bases', branch))
         set_format_version(2)
 
+    # Update 2 -> 3.
+    if get_format_version() == 2:
+        patchorder = file(os.path.join(branch_dir, 'patchorder'), 'w')
+        for pf in ['applied', 'unapplied']:
+            pfn = os.path.join(branch_dir, pf)
+            if not os.path.isfile(pfn):
+                continue
+            for line in file(pfn):
+                line = line.strip()
+                if line:
+                    patchorder.write(line + '\n')
+            rm(pfn)
+        patchorder.close()
+        set_format_version(3)
+
     # Make sure we're at the latest version.
     if not get_format_version() in [None, FORMAT_VERSION]:
         raise StackException('Branch %s is at format version %d, expected %d'
                              % (branch, get_format_version(), FORMAT_VERSION))
 
+class PatchorderCache:
+    """An object that keeps track of the patch order for a series, as
+    saved in its patchorder file."""
+    def __init__(self, series):
+        self.__series = series
+        self.__invalidate()
+    def __invalidate(self):
+        self.__patchnames = None
+        self.__position = None
+    def __cache(self):
+        if self.__patchnames != None:
+            return # already cached
+
+        self.__patchnames = []
+        self.__position = {}
+        pof = os.path.join(self.__series._dir(), 'patchorder')
+        if os.path.isfile(pof):
+            for line in file(pof):
+                name = line.strip()
+                assert not name in self.__position
+                self.__position[name] = len(self.__patchnames)
+                self.__patchnames.append(name)
+    def set_patchorder(self, new_order):
+        self.__invalidate()
+        f = file(os.path.join(self.__series._dir(), 'patchorder'), 'w')
+        for name in new_order:
+            f.write('%s\n' % name)
+        f.close()
+    def cmp(self, name1, name2):
+        """Compare two patch names to see which patch comes first. If
+        both patches are listed in the patchorder file, sort them by
+        the order they appear there; if one is listed and the other
+        not, the listed patch goes first; and if neither is listed,
+        sort them by their names."""
+        self.__cache()
+        largepos = len(self.__patchnames)
+        pos1 = self.__position.get(name1, largepos)
+        pos2 = self.__position.get(name2, largepos)
+        return cmp((pos1, name1), (pos2, name2))
+
+def read_refs(branch):
+    """Return a mapping from patches and branch head to hashes for a
+    given branch. The patches are listed by name; the branch head is
+    None."""
+    refs = {}
+    patchpat = re.compile(r'^refs/patches/%s/([^\.]+)$' % branch)
+    for line in git._output_lines('git-show-ref'):
+        sha1, ref = line.split()
+        m = re.match(patchpat, ref)
+        if m:
+            refs[m.group(1)] = sha1
+        elif ref == 'refs/heads/%s' % branch:
+            refs[None] = sha1
+    return refs
+
+def unapplied_patches(ref2hash):
+    """Given a map of patch names (and the branch head, keyed by None)
+    to hashes, return the set of unapplied patches."""
+    hash2refs = {}
+    for r, h in ref2hash.iteritems():
+        hash2refs.setdefault(h, Set()).add(r)
+
+    unapplied = Set()
+    for line in git._output_lines(
+        'git-rev-list --stdin',
+        ('%s%s\n' % (['', '^'][ref == None], sha1)
+         for ref, sha1 in ref2hash.iteritems())):
+        for ref in hash2refs.get(line.strip(), []):
+            unapplied.add(ref)
+    return unapplied
+
+def sort_applied_patches(ref2hash):
+    """Given a map of patch names (and the branch head, keyed by None)
+    to hashes, return a list with the applied patches in stack order.
+    All patches in the map must be applied."""
+    hash2refs = {}
+    for r, h in ref2hash.iteritems():
+        if r != None:
+            hash2refs.setdefault(h, Set()).add(r)
+
+    missing = Set(ref for ref in ref2hash.iterkeys() if ref != None)
+    if not missing:
+        return []
+    applied = []
+    grl = popen2.Popen3('git-rev-list %s' % ref2hash[None], True)
+    for line in grl.fromchild:
+        for ref in hash2refs.get(line.strip(), []):
+            applied.append(ref)
+            missing.remove(ref)
+        if not missing:
+            applied.reverse()
+            return applied
+
+    raise StackException, 'Could not find patches: %s' % ', '.join(missing)
+
+class AppliedCache:
+    """An object that keeps track of the appliedness and order of the
+    patches in a patch series."""
+    def __init__(self, series):
+        self.__series = series
+        self.__order = PatchorderCache(series)
+        self.__invalidate()
+    def get_applied(self):
+        self.__cache()
+        return self.__applied
+    def get_unapplied(self):
+        self.__cache()
+        return self.__unapplied
+    def rename(self, oldname, newname):
+        """Rename a patch."""
+        self.__cache()
+        for lst in (self.__applied, self.__unapplied):
+            try:
+                lst[lst.index(oldname)] = newname
+            except ValueError:
+                pass # lst.index() couldn't find the index
+            else:
+                self.__write_patchorder()
+                return
+        raise StackException, 'Unknown patch "%s"' % oldname
+    def __write_patchorder(self):
+        self.__order.set_patchorder(self.get_applied() + self.get_unapplied())
+    def set_patchorder(self, new_order):
+        self.__order.set_patchorder(new_order)
+        self.refresh()
+    def refresh(self):
+        """Re-read patch appliedness info, and write patch order to
+        disk."""
+        self.__invalidate()
+        self.__write_patchorder()
+    def __invalidate(self):
+        self.__applied = None
+        self.__unapplied = None
+    def __cached(self):
+        return (self.__applied != None)
+    def __cache(self):
+        if self.__cached():
+            return
+        patches = read_refs(self.__series.get_branch())
+        unapplied = unapplied_patches(patches)
+        for patch in unapplied:
+            del patches[patch]
+        self.__applied = sort_applied_patches(patches)
+        self.__unapplied = list(unapplied)
+        self.__unapplied.sort(self.__order.cmp)
+
+
 class Series(StgitObject):
     """Class including the operations on series
     """
@@ -372,8 +535,6 @@ class Series(StgitObject):
         self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
                                        self.__name)
 
-        self.__applied_file = os.path.join(self._dir(), 'applied')
-        self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
         self.__hidden_file = os.path.join(self._dir(), 'hidden')
 
         # where this series keeps its patches
@@ -382,6 +543,8 @@ class Series(StgitObject):
         # trash directory
         self.__trash_dir = os.path.join(self._dir(), 'trash')
 
+        self.__applied_cache = AppliedCache(self)
+
     def __patch_name_valid(self, name):
         """Raise an exception if the patch name is not valid.
         """
@@ -409,11 +572,7 @@ class Series(StgitObject):
     def get_current(self):
         """Return the name of the topmost patch, or None if there is
         no such patch."""
-        try:
-            applied = self.get_applied()
-        except StackException:
-            # No "applied" file: branch is not initialized.
-            return None
+        applied = self.get_applied()
         try:
             return applied[-1]
         except IndexError:
@@ -421,20 +580,10 @@ class Series(StgitObject):
             return None
 
     def get_applied(self):
-        if not os.path.isfile(self.__applied_file):
-            raise StackException, 'Branch "%s" not initialised' % self.__name
-        f = file(self.__applied_file)
-        names = [line.strip() for line in f.readlines()]
-        f.close()
-        return names
+        return self.__applied_cache.get_applied()
 
     def get_unapplied(self):
-        if not os.path.isfile(self.__unapplied_file):
-            raise StackException, 'Branch "%s" not initialised' % self.__name
-        f = file(self.__unapplied_file)
-        names = [line.strip() for line in f.readlines()]
-        f.close()
-        return names
+        return self.__applied_cache.get_unapplied()
 
     def get_hidden(self):
         if not os.path.isfile(self.__hidden_file):
@@ -446,12 +595,12 @@ class Series(StgitObject):
 
     def get_base(self):
         # Return the parent of the bottommost patch, if there is one.
-        if os.path.isfile(self.__applied_file):
-            bottommost = file(self.__applied_file).readline().strip()
-            if bottommost:
-                return self.get_patch(bottommost).get_bottom()
-        # No bottommost patch, so just return HEAD
-        return git.get_head()
+        applied = self.get_applied()
+        if applied:
+            return self.get_patch(applied[0]).get_bottom()
+        else:
+            # No bottommost patch, so just return HEAD
+            return git.get_head()
 
     def get_head(self):
         """Return the head of the branch
@@ -585,8 +734,6 @@ class Series(StgitObject):
 
         self.set_parent(parent_remote, parent_branch)
 
-        self.create_empty_field('applied')
-        self.create_empty_field('unapplied')
         os.makedirs(self.__refs_dir)
         self._set_field('orig-base', git.get_head())
 
@@ -687,10 +834,6 @@ class Series(StgitObject):
 
             # FIXME: find a way to get rid of those manual removals
             # (move functionality to StgitObject ?)
-            if os.path.exists(self.__applied_file):
-                os.remove(self.__applied_file)
-            if os.path.exists(self.__unapplied_file):
-                os.remove(self.__unapplied_file)
             if os.path.exists(self.__hidden_file):
                 os.remove(self.__hidden_file)
             if os.path.exists(self._dir()+'/orig-base'):
@@ -719,7 +862,7 @@ class Series(StgitObject):
         config.unset('branch.%s.merge' % self.__name)
         config.unset('branch.%s.stgit.parentbranch' % self.__name)
 
-    def refresh_patch(self, files = None, message = None, edit = False,
+    def refresh_patch(self, name, files = None, message = None, edit = False,
                       show_patch = False,
                       cache_update = True,
                       author_name = None, author_email = None,
@@ -728,10 +871,6 @@ class Series(StgitObject):
                       backup = False, sign_str = None, log = 'refresh'):
         """Generates a new commit for the given patch
         """
-        name = self.get_current()
-        if not name:
-            raise StackException, 'No patches applied'
-
         patch = self.get_patch(name)
 
         descr = patch.get_description()
@@ -821,9 +960,10 @@ class Series(StgitObject):
         """Creates a new patch
         """
 
+        appl, unappl = self.get_applied(), self.get_unapplied()
         if name != None:
             self.__patch_name_valid(name)
-            if self.patch_applied(name) or self.patch_unapplied(name):
+            if name in appl or name in unappl:
                 raise StackException, 'Patch "%s" already exists' % name
 
         if not message and can_edit:
@@ -860,20 +1000,29 @@ class Series(StgitObject):
 
         if unapplied:
             self.log_patch(patch, 'new')
-
-            patches = [patch.get_name()] + self.get_unapplied()
-
-            f = file(self.__unapplied_file, 'w+')
-            f.writelines([line + '\n' for line in patches])
-            f.close()
+            order = appl + [patch.get_name()] + unappl
         elif before_existing:
             self.log_patch(patch, 'new')
-
-            insert_string(self.__applied_file, patch.get_name())
+            order = [patch.get_name()] + appl + unappl
         else:
-            append_string(self.__applied_file, patch.get_name())
+            order = appl + [patch.get_name()] + unappl
             if refresh:
-                self.refresh_patch(cache_update = False, log = 'new')
+                self.refresh_patch(name, cache_update = False, log = 'new')
+        self.__applied_cache.set_patchorder(order)
+
+        return patch
+
+
+    def delete_patch_data(self, name):
+        """Deletes the stgit data for a patch."""
+        patch = Patch(name, self.__patch_dir, self.__refs_dir)
+
+        # save the commit id to a trash file
+        write_string(os.path.join(self.__trash_dir, name), patch.get_top())
+
+        patch.delete()
+        if self.patch_hidden(name):
+            self.unhide_patch(name)
 
         return patch
 
@@ -881,9 +1030,8 @@ class Series(StgitObject):
         """Deletes a patch
         """
         self.__patch_name_valid(name)
-        patch = Patch(name, self.__patch_dir, self.__refs_dir)
 
-        if self.__patch_is_current(patch):
+        if self.get_current() == name:
             self.pop_patch(name)
         elif self.patch_applied(name):
             raise StackException, 'Cannot remove an applied patch, "%s", ' \
@@ -891,19 +1039,8 @@ class Series(StgitObject):
         elif not name in self.get_unapplied():
             raise StackException, 'Unknown patch "%s"' % name
 
-        # save the commit id to a trash file
-        write_string(os.path.join(self.__trash_dir, name), patch.get_top())
-
-        patch.delete()
-
-        unapplied = self.get_unapplied()
-        unapplied.remove(name)
-        f = file(self.__unapplied_file, 'w+')
-        f.writelines([line + '\n' for line in unapplied])
-        f.close()
-
-        if self.patch_hidden(name):
-            self.unhide_patch(name)
+        self.delete_patch_data(name)
+        self.__applied_cache.refresh()
 
     def forward_patches(self, names):
         """Try to fast-forward an array of patches.
@@ -967,19 +1104,12 @@ class Series(StgitObject):
                     break
 
             forwarded+=1
-            unapplied.remove(name)
 
         if forwarded == 0:
             return 0
 
         git.switch(top)
-
-        append_strings(self.__applied_file, names[0:forwarded])
-
-        f = file(self.__unapplied_file, 'w+')
-        f.writelines([line + '\n' for line in unapplied])
-        f.close()
-
+        self.__applied_cache.refresh()
         return forwarded
 
     def merged_patches(self, names):
@@ -1052,13 +1182,6 @@ class Series(StgitObject):
                               'Use "refresh" after fixing the conflicts or'
                               ' revert the operation with "push --undo".')
 
-        append_string(self.__applied_file, name)
-
-        unapplied.remove(name)
-        f = file(self.__unapplied_file, 'w+')
-        f.writelines([line + '\n' for line in unapplied])
-        f.close()
-
         # head == bottom case doesn't need to refresh the patch
         if empty or head != bottom:
             if not ex:
@@ -1068,15 +1191,17 @@ class Series(StgitObject):
                     log = 'push(m)'
                 else:
                     log = 'push'
-                self.refresh_patch(cache_update = False, log = log)
+                self.refresh_patch(name, 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(cache_update = False, log = 'push(c)')
+                self.refresh_patch(name, cache_update = False, log = 'push(c)')
                 raise StackException, str(ex)
 
+        self.__applied_cache.refresh()
+
         return modified
 
     def undo_push(self):
@@ -1105,10 +1230,7 @@ class Series(StgitObject):
     def pop_patch(self, name, keep = False):
         """Pops the top patch from the stack
         """
-        applied = self.get_applied()
-        applied.reverse()
-        assert(name in applied)
-
+        assert(name in self.get_applied())
         patch = self.get_patch(name)
 
         if git.get_head_file() == self.get_branch():
@@ -1118,24 +1240,7 @@ class Series(StgitObject):
             git.switch(patch.get_bottom(), keep)
         else:
             git.set_branch(self.get_branch(), patch.get_bottom())
-
-        # save the new applied list
-        idx = applied.index(name) + 1
-
-        popped = applied[:idx]
-        popped.reverse()
-        unapplied = popped + self.get_unapplied()
-
-        f = file(self.__unapplied_file, 'w+')
-        f.writelines([line + '\n' for line in unapplied])
-        f.close()
-
-        del applied[:idx]
-        applied.reverse()
-
-        f = file(self.__applied_file, 'w+')
-        f.writelines([line + '\n' for line in applied])
-        f.close()
+        self.__applied_cache.refresh()
 
     def empty_patch(self, name):
         """Returns True if the patch is empty
@@ -1161,7 +1266,8 @@ class Series(StgitObject):
 
         if oldname == newname:
             raise StackException, '"To" name and "from" name are the same'
-
+        if oldname in applied or oldname in unapplied:
+            raise StackException, 'Unknown patch "%s"' % oldname
         if newname in applied or newname in unapplied:
             raise StackException, 'Patch "%s" already exists' % newname
 
@@ -1169,23 +1275,8 @@ class Series(StgitObject):
             self.unhide_patch(oldname)
             self.hide_patch(newname)
 
-        if oldname in unapplied:
-            self.get_patch(oldname).rename(newname)
-            unapplied[unapplied.index(oldname)] = newname
-
-            f = file(self.__unapplied_file, 'w+')
-            f.writelines([line + '\n' for line in unapplied])
-            f.close()
-        elif oldname in applied:
-            self.get_patch(oldname).rename(newname)
-
-            applied[applied.index(oldname)] = newname
-
-            f = file(self.__applied_file, 'w+')
-            f.writelines([line + '\n' for line in applied])
-            f.close()
-        else:
-            raise StackException, 'Unknown patch "%s"' % oldname
+        self.get_patch(oldname).rename(newname)
+        self.__applied_cache.rename(oldname, newname)
 
     def log_patch(self, patch, message):
         """Generate a log commit for a patch
diff --git a/t/t4000-upgrade.sh b/t/t4000-upgrade.sh
index 8a308fb..01be50d 100755
--- a/t/t4000-upgrade.sh
+++ b/t/t4000-upgrade.sh
@@ -34,6 +34,12 @@ for ver in 0.12 0.8; do
         ! git show-ref --verify --quiet refs/bases/master
     '
 
+    test_expect_success \
+        "v$ver: Make sure the applied and unapplied files are gone" '
+        [ ! -e .git/patches/master/applied ] &&
+        [ ! -e .git/patches/master/unapplied ]
+'
+
     cd ..
 done
 
Karl Hasselström· Jun 10, 2007, 09:55 UTC · re: Karl Hasselström · lore

[StGIT PATCH 4/6] Test the new DAG appliedness machinery

Signed-off-by: Karl Hasselström <kha@treskal.com>
---
 t/t3000-git-interop.sh |   60 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 60 insertions(+), 0 deletions(-)
Show changes to t/t3000-git-interop.sh +60 −0
diff --git a/t/t3000-git-interop.sh b/t/t3000-git-interop.sh
new file mode 100755
index 0000000..44414b9
--- /dev/null
+++ b/t/t3000-git-interop.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+# Copyright (c) 2007 Karl Hasselström
+test_description='Test git/StGIT interoperability'
+. ./test-lib.sh
+
+test_expect_success \
+    'Create some git-only history' '
+    echo foo > foo.txt &&
+    git add foo.txt &&
+    git commit -a -m foo &&
+    git tag foo-tag &&
+    for i in 0 1 2 3 4; do
+        echo foo$i >> foo.txt &&
+        git commit -a -m foo$i;
+    done
+'
+
+test_expect_success \
+    'Initialize the StGIT repository' '
+    stg init
+'
+
+test_expect_success \
+    'Create five patches' '
+    for i in 0 1 2 3 4; do
+        stg new p$i -m p$i;
+    done &&
+    [ "$(echo $(stg applied))" = "p0 p1 p2 p3 p4" ] &&
+    [ "$(echo $(stg unapplied))" = "" ]
+'
+
+test_expect_success \
+    'Pop two patches with git-reset' '
+    git reset --hard HEAD~2 &&
+    [ "$(echo $(stg applied))" = "p0 p1 p2" ] &&
+    [ "$(echo $(stg unapplied))" = "p3 p4" ]
+'
+
+test_expect_success \
+    'Create a new patch' '
+    stg new q0 -m q0 &&
+    [ "$(echo $(stg applied))" = "p0 p1 p2 q0" ] &&
+    [ "$(echo $(stg unapplied))" = "p3 p4" ]
+'
+
+test_expect_success \
+    'Go to an unapplied patch with with git-reset' '
+    git reset --hard $(stg id p3) &&
+    [ "$(echo $(stg applied))" = "p0 p1 p2 p3" ] &&
+    [ "$(echo $(stg unapplied))" = "q0 p4" ]
+'
+
+test_expect_success \
+    'Go back to below the stack base with git-reset' '
+    git reset --hard foo-tag &&
+    [ "$(echo $(stg applied))" = "" ] &&
+    [ "$(echo $(stg unapplied))" = "p0 p1 p2 q0 p3 p4" ]
+'
+
+test_done
Karl Hasselström· Jun 10, 2007, 09:55 UTC · re: Karl Hasselström · lore

[StGIT PATCH 5/6] Fix bash completion after the DAG appliedness patch

The bash tab completion used the "applied", "unapplied" and "current" files to generate completions. Since these don't exist anymore, use stg applied/unapplied/series to obtain the same info. It's a bit slower, but not terribly much so.

Signed-off-by: Karl Hasselström <kha@treskal.com>
---
 contrib/stgit-completion.bash |   15 ++++-----------
 1 files changed, 4 insertions(+), 11 deletions(-)
Show changes to contrib/stgit-completion.bash +4 −11
diff --git a/contrib/stgit-completion.bash b/contrib/stgit-completion.bash
index d497098..2b9722a 100644
--- a/contrib/stgit-completion.bash
+++ b/contrib/stgit-completion.bash
@@ -70,32 +70,25 @@ _current_branch ()
 # List of all applied patches.
 _applied_patches ()
 {
-    local g=$(_gitdir)
-    [ "$g" ] && cat "$g/patches/$(_current_branch)/applied"
+    stg applied 2> /dev/null
 }
 
 # List of all unapplied patches.
 _unapplied_patches ()
 {
-    local g=$(_gitdir)
-    [ "$g" ] && cat "$g/patches/$(_current_branch)/unapplied"
+    stg unapplied 2> /dev/null
 }
 
 # List of all patches.
 _all_patches ()
 {
-    local b=$(_current_branch)
-    local g=$(_gitdir)
-    [ "$g" ] && cat "$g/patches/$b/applied" "$g/patches/$b/unapplied"
+    stg series --noprefix 2> /dev/null
 }
 
 # List of all patches except the current patch.
 _all_other_patches ()
 {
-    local b=$(_current_branch)
-    local g=$(_gitdir)
-    [ "$g" ] && cat "$g/patches/$b/applied" "$g/patches/$b/unapplied" \
-        | grep -v "^$(cat $g/patches/$b/current 2> /dev/null)$"
+    stg series 2> /dev/null | grep -v '^>' | cut -f 2 -d ' '
 }
 
 _all_branches ()
Karl Hasselström· Jun 10, 2007, 09:55 UTC · re: Karl Hasselström · lore

[StGIT PATCH 6/6] Speed up the appliedness test

The appliedness test was too slow if at least one patch, applied or unapplied, was too far away from HEAD, since we had to visit the whole intervening part of the commit DAG.

This patch fixes that problem by maintaining a cache of uninteresting commits that are known to not reach any patches in the commit DAG. (Specifically, this is at all times the set of commits that are parents to patch commits and do not have a patch commit as their ancestor.) By exlcuding these commits when walking the graph, we only have to visit the interesting places.

As a nice side effect, the cache of uninteresting commits makes it possible to use just one git-rev-list call instead of two, since we can list the applied patches without first computing the unapplied patches; the unapplied patches are then simply all patches except those that are applied.

Signed-off-by: Karl Hasselström <kha@treskal.com>
---
 stgit/stack.py |  278 +++++++++++++++++++++++++++++++++++++++++++++-----------
 1 files changed, 223 insertions(+), 55 deletions(-)
Show changes to stgit/stack.py +223 −55
diff --git a/stgit/stack.py b/stgit/stack.py
index e622f68..6fd1a2a 100644
--- a/stgit/stack.py
+++ b/stgit/stack.py
@@ -156,7 +156,7 @@ class Patch(StgitObject):
         os.mkdir(self._dir())
         self.create_empty_field('bottom')
         self.create_empty_field('top')
-
+ 
     def delete(self):
         for f in os.listdir(self._dir()):
             os.remove(os.path.join(self._dir(), f))
@@ -369,7 +369,11 @@ class PatchorderCache:
     saved in its patchorder file."""
     def __init__(self, series):
         self.__series = series
+        self.__file = os.path.join(self.__series._dir(), 'patchorder')
         self.__invalidate()
+    def delete_file(self):
+        if os.path.isfile(self.__file):
+            os.remove(self.__file)
     def __invalidate(self):
         self.__patchnames = None
         self.__position = None
@@ -379,9 +383,8 @@ class PatchorderCache:
 
         self.__patchnames = []
         self.__position = {}
-        pof = os.path.join(self.__series._dir(), 'patchorder')
-        if os.path.isfile(pof):
-            for line in file(pof):
+        if os.path.isfile(self.__file):
+            for line in file(self.__file):
                 name = line.strip()
                 assert not name in self.__position
                 self.__position[name] = len(self.__patchnames)
@@ -404,60 +407,200 @@ class PatchorderCache:
         pos2 = self.__position.get(name2, largepos)
         return cmp((pos1, name1), (pos2, name2))
 
+class UninterestingCache:
+    """Keeps track of a set of commits that do not reach any patches.
+    These are used to speed up the detection of unapplied patches.
+
+    Specifically, this is at all times the set of commits c that
+    fulfill the following two criteria:
+
+      * c does not reach any patch
+
+      * c is the parent of a patch
+
+    """
+    def __init__(self, series):
+        self.__series = series
+        self.__uninteresting = None
+        self.__filename = os.path.join(self.__series._dir(), 'uninteresting')
+    def __invalidate(self):
+        self.__uninteresting = None
+        self.delete_file()
+    def delete_file(self):
+        if os.path.isfile(self.__filename):
+            os.remove(self.__filename)
+    def __other_patches(self, patchname):
+        """All patches except the named one."""
+        ref2hash = read_refs(self.__series.get_branch())
+        return [self.__series.get_patch(ref)
+                for ref in ref2hash.iterkeys()
+                if ref and ref != patchname]
+    def __write_file(self):
+        """Write the uninteresting commits to file."""
+        try:
+            f = file(self.__filename, 'w')
+            for u in self.__uninteresting:
+                f.write('%s\n' % u)
+            f.close()
+        except IOError:
+            pass # this isn't fatal -- the directory is probably missing
+    def __read_file(self):
+        """Read the uninteresting commits from file. Return true on
+        success, false on failure."""
+        if not os.path.isfile(self.__filename):
+            return False
+        self.__uninteresting = Set()
+        for line in file(self.__filename):
+            self.__uninteresting.add(line.strip())
+        return True
+    def __cache_file(self):
+        """Try to cache the uninteresting commits using only the cache
+        file. Return true on success, false on failure."""
+        if self.__uninteresting != None:
+            return True # already cached
+        return self.__read_file()
+    def __cache(self):
+        """Cache the uninteresting commits, recomputing them if
+        necessary."""
+        if self.__cache_file():
+            return
+        self.__compute_uninteresting()
+        self.__write_file()
+    def __compute_uninteresting(self):
+        """Compute a reasonable set of uninteresting commits from
+        scratch. This is expensive."""
+        out.start('Finding uninteresting commits')
+        ref2hash = read_refs(self.__series.get_branch())
+        patches = Set([sha1 for ref, sha1 in ref2hash.iteritems() if ref])
+        interesting, uninteresting = Set(), Set()
+
+        # Iterate over all commits. We are guaranteed to see each
+        # commit before any of its children.
+        for line in git._output_lines(
+            'git-rev-list --topo-order --reverse --parents --all'):
+            commits = line.split()
+            commit, parents = commits[0], Set(commits[1:])
+
+            # Patches are interesting.
+            if commit in patches:
+                interesting.add(commit)
+
+                # The parents of a patch are uninteresting unless they
+                # are interesting.
+                for p in parents:
+                    if not p in interesting:
+                        uninteresting.add(p)
+                continue
+
+            # Commits with interesting parents are interesting.
+            if interesting.intersection(parents):
+                interesting.add(commit)
+        self.__uninteresting = uninteresting
+        out.done()
+    def create_patch(self, name, top, bottom):
+        """The given patch has been created. Update the uninterested
+        state to maintain the invariant."""
+        if not self.__cache_file():
+            return # not cached
+
+        # New patch inserted just below an existing bottommost patch:
+        # need to move the uninteresting commit down one step.
+        if top in self.__uninteresting:
+            self.__uninteresting.remove(top)
+            self.__uninteresting.add(bottom)
+            self.__write_file()
+            return
+
+        # New patch inserted next to an existing non-bottommost patch:
+        # don't need to do anything.
+        existing_patches = self.__other_patches(name)
+        tops = Set([p.get_top() for p in existing_patches])
+        bottoms = Set([p.get_bottom() for p in existing_patches])
+        if bottom in bottoms or bottom in tops or top in bottoms:
+            return
+
+        # The new patch is not adjacent to an existing patch. We'd
+        # need to first get rid of any uninteresting commit that
+        # reaches this patch, and then mark the patch's bottom
+        # uninteresting if it doesn't reach any other patch. This is a
+        # lot of work, so we chicken out and blow the whole cache
+        # instead.
+        self.__invalidate()
+    def delete_patch(self, name, top, bottom):
+        """The given patch has been deleted. Update the uninterested
+        state to maintain the invariant."""
+        if not self.__cache_file():
+            return # not cached
+
+        # If this patch reaches another patch, there's nothing to do.
+        if not bottom in self.__uninteresting:
+            return
+
+        # If another patch has the same bottom, it's still
+        # uninteresting and there's nothing more to do.
+        other_patches = self.__other_patches(name)
+        for p in other_patches:
+            if p.get_bottom() == bottom:
+                return
+
+        # If there are other patches on top of this one, their bottoms
+        # (this patch's top) become uninteresting in place of this
+        # patch's bottom.
+        for p in other_patches:
+            if p.get_bottom() == top:
+                self.__uninteresting.remove(bottom)
+                self.__uninteresting.add(top)
+                self.__write_file()
+                return
+
+        # The bottom of this patch is no longer uninteresting. But
+        # there might be other patches that reach it, whose bottoms
+        # would need to be marked uninteresting. That would require an
+        # expensive reachability analysis.
+        self.__invalidate()
+    def get(self):
+        self.__cache()
+        return self.__uninteresting
+
 def read_refs(branch):
     """Return a mapping from patches and branch head to hashes for a
     given branch. The patches are listed by name; the branch head is
     None."""
     refs = {}
     patchpat = re.compile(r'^refs/patches/%s/([^\.]+)$' % branch)
+    head = 'refs/heads/%s' % branch
     for line in git._output_lines('git-show-ref'):
         sha1, ref = line.split()
         m = re.match(patchpat, ref)
         if m:
             refs[m.group(1)] = sha1
-        elif ref == 'refs/heads/%s' % branch:
+        elif ref == head:
             refs[None] = sha1
+    if not None in refs:
+        raise StackException, 'Could not find %s' % head
     return refs
 
-def unapplied_patches(ref2hash):
+def get_patches(ref2hash, uninteresting):
     """Given a map of patch names (and the branch head, keyed by None)
-    to hashes, return the set of unapplied patches."""
-    hash2refs = {}
-    for r, h in ref2hash.iteritems():
-        hash2refs.setdefault(h, Set()).add(r)
-
+    to hashes, return the list of applied patches and the set of
+    unapplied patches. The second parameter is a set of commit objects
+    that do not reach any patch."""
+    applied = []
     unapplied = Set()
-    for line in git._output_lines(
-        'git-rev-list --stdin',
-        ('%s%s\n' % (['', '^'][ref == None], sha1)
-         for ref, sha1 in ref2hash.iteritems())):
-        for ref in hash2refs.get(line.strip(), []):
-            unapplied.add(ref)
-    return unapplied
-
-def sort_applied_patches(ref2hash):
-    """Given a map of patch names (and the branch head, keyed by None)
-    to hashes, return a list with the applied patches in stack order.
-    All patches in the map must be applied."""
-    hash2refs = {}
+    hash2patches = {}
     for r, h in ref2hash.iteritems():
-        if r != None:
-            hash2refs.setdefault(h, Set()).add(r)
+        if r:
+            hash2patches.setdefault(h, Set()).add(r)
+            unapplied.add(r)
 
-    missing = Set(ref for ref in ref2hash.iterkeys() if ref != None)
-    if not missing:
-        return []
-    applied = []
-    grl = popen2.Popen3('git-rev-list %s' % ref2hash[None], True)
-    for line in grl.fromchild:
-        for ref in hash2refs.get(line.strip(), []):
+    for line in git._output_lines(
+        'git-rev-list --topo-order --stdin', ['%s\n' % ref2hash[None]]
+        + ['^%s\n' % u for u in uninteresting]):
+        for ref in hash2patches.get(line.strip(), []):
             applied.append(ref)
-            missing.remove(ref)
-        if not missing:
-            applied.reverse()
-            return applied
-
-    raise StackException, 'Could not find patches: %s' % ', '.join(missing)
+            unapplied.remove(ref)
+    applied.reverse()
+    return applied, unapplied
 
 class AppliedCache:
     """An object that keeps track of the appliedness and order of the
@@ -465,7 +608,11 @@ class AppliedCache:
     def __init__(self, series):
         self.__series = series
         self.__order = PatchorderCache(series)
+        self.__uninteresting = UninterestingCache(series)
         self.__invalidate()
+    def delete_files(self):
+        for sub in [self.__uninteresting, self.__order]:
+            sub.delete_file()
     def get_applied(self):
         self.__cache()
         return self.__applied
@@ -484,6 +631,17 @@ class AppliedCache:
                 self.__write_patchorder()
                 return
         raise StackException, 'Unknown patch "%s"' % oldname
+    def new(self, name, top, bottom):
+        """Create new patch."""
+        self.__uninteresting.create_patch(name, top, bottom)
+    def delete(self, name, top, bottom):
+        """Delete a patch."""
+        self.__uninteresting.delete_patch(name, top, bottom)
+    def change(self, name, old_top, old_bottom, new_top, new_bottom):
+        """Change a patch."""
+        if (new_top, new_bottom) != (old_top, old_bottom):
+            self.new(name, new_top, new_bottom)
+            self.delete(name, old_top, old_bottom)
     def __write_patchorder(self):
         self.__order.set_patchorder(self.get_applied() + self.get_unapplied())
     def set_patchorder(self, new_order):
@@ -502,11 +660,8 @@ class AppliedCache:
     def __cache(self):
         if self.__cached():
             return
-        patches = read_refs(self.__series.get_branch())
-        unapplied = unapplied_patches(patches)
-        for patch in unapplied:
-            del patches[patch]
-        self.__applied = sort_applied_patches(patches)
+        self.__applied, unapplied = get_patches(
+            read_refs(self.__series.get_branch()), self.__uninteresting.get())
         self.__unapplied = list(unapplied)
         self.__unapplied.sort(self.__order.cmp)
 
@@ -838,6 +993,7 @@ class Series(StgitObject):
                 os.remove(self.__hidden_file)
             if os.path.exists(self._dir()+'/orig-base'):
                 os.remove(self._dir()+'/orig-base')
+            self.__applied_cache.delete_files()
 
             if not os.listdir(self.__patch_dir):
                 os.rmdir(self.__patch_dir)
@@ -940,16 +1096,20 @@ class Series(StgitObject):
         patch = self.get_patch(name)
         old_bottom = patch.get_old_bottom()
         old_top = patch.get_old_top()
+        curr_bottom = patch.get_bottom()
+        curr_top = patch.get_top()
 
         # the bottom of the patch is not changed by refresh. If the
         # old_bottom is different, there wasn't any previous 'refresh'
         # command (probably only a 'push')
-        if old_bottom != patch.get_bottom() or old_top == patch.get_top():
+        if old_bottom != curr_bottom or old_top == curr_top:
             raise StackException, 'No undo information available'
 
         git.reset(tree_id = old_top, check_out = False)
         if patch.restore_old_boundaries():
             self.log_patch(patch, 'undo')
+        self.__applied_cache.change(name, curr_top, curr_bottom,
+                                    old_top, old_bottom)
 
     def new_patch(self, name, message = None, can_edit = True,
                   unapplied = False, show_patch = False,
@@ -982,14 +1142,11 @@ class Series(StgitObject):
         patch = self.get_patch(name)
         patch.create()
 
-        if bottom:
-            patch.set_bottom(bottom)
-        else:
-            patch.set_bottom(head)
-        if top:
-            patch.set_top(top)
-        else:
-            patch.set_top(head)
+        bottom = bottom or head
+        top = top or head
+        patch.set_bottom(bottom)
+        patch.set_top(top)
+        self.__applied_cache.new(name, top, bottom)
 
         patch.set_description(descr)
         patch.set_authname(author_name)
@@ -1016,15 +1173,16 @@ class Series(StgitObject):
     def delete_patch_data(self, name):
         """Deletes the stgit data for a patch."""
         patch = Patch(name, self.__patch_dir, self.__refs_dir)
+        top, bottom = patch.get_top(), patch.get_bottom()
 
         # save the commit id to a trash file
-        write_string(os.path.join(self.__trash_dir, name), patch.get_top())
+        write_string(os.path.join(self.__trash_dir, name), top)
 
         patch.delete()
         if self.patch_hidden(name):
             self.unhide_patch(name)
 
-        return patch
+        self.__applied_cache.delete(name, top, bottom)
 
     def delete_patch(self, name):
         """Deletes a patch
@@ -1084,6 +1242,7 @@ class Series(StgitObject):
 
                     top_tree = git.get_commit(top).get_tree()
 
+                    old_top = top
                     top = git.commit(message = descr, parents = [head],
                                      cache_update = False,
                                      tree_id = top_tree,
@@ -1097,6 +1256,9 @@ class Series(StgitObject):
                     patch.set_bottom(head, backup = True)
                     patch.set_top(top, backup = True)
 
+                    self.__applied_cache.change(
+                        name, old_top = old_top, old_bottom = bottom,
+                        new_top = top, new_bottom = head)
                     self.log_patch(patch, 'push(f)')
                 else:
                     top = head
@@ -1154,6 +1316,7 @@ class Series(StgitObject):
             # need an empty commit
             patch.set_bottom(head, backup = True)
             patch.set_top(head, backup = True)
+            self.__applied_cache.change(name, top, bottom, head, head)
             modified = True
         elif head == bottom:
             # reset the backup information. No need for logging
@@ -1166,6 +1329,7 @@ class Series(StgitObject):
             # The current patch is empty after merge.
             patch.set_bottom(head, backup = True)
             patch.set_top(head, backup = True)
+            self.__applied_cache.change(name, top, bottom, head, head)
 
             # Try the fast applying first. If this fails, fall back to the
             # three-way merge
@@ -1211,6 +1375,8 @@ class Series(StgitObject):
         patch = self.get_patch(name)
         old_bottom = patch.get_old_bottom()
         old_top = patch.get_old_top()
+        curr_bottom = patch.get_bottom()
+        curr_top = patch.get_top()
 
         # the top of the patch is changed by a push operation only
         # together with the bottom (otherwise the top was probably
@@ -1222,6 +1388,8 @@ class Series(StgitObject):
         git.reset()
         self.pop_patch(name)
         ret = patch.restore_old_boundaries()
+        self.__applied_cache.change(name, curr_top, curr_bottom,
+                                    old_top, old_bottom)
         if ret:
             self.log_patch(patch, 'undo')
 
Yann Dirson· Jun 30, 2007, 19:54 UTC · re: Karl Hasselström · lore

Re: [StGIT PATCH 0/6] New and improved DAG appliedness series

Hi Karl,
On Sun, Jun 10, 2007 at 02:54:47AM -0700, Karl Hasselström wrote:
> This took a while, but here it is. (Actually, I finished this about a
> week ago, but had no Internet connection so I couldn't send it.)

Is this the latest version of the DAG patches, or is there maybe a public repo where you push your work ?

It happens that my refactorings touches virtually everything, so there will be conflicts, and the best thing to do is probably that I rebase my work on yours.

Best regards,
-- 
Yann
Karl Hasselström· Jul 1, 2007, 14:35 UTC · re: Yann Dirson · lore

Re: [StGIT PATCH 0/6] New and improved DAG appliedness series

On 2007-06-30 21:54:51 +0200, Yann Dirson wrote:
Show 8 quoted lines
> On Sun, Jun 10, 2007 at 02:54:47AM -0700, Karl Hasselström wrote:
>
> > This took a while, but here it is. (Actually, I finished this
> > about a week ago, but had no Internet connection so I couldn't
> > send it.)
>
> Is this the latest version of the DAG patches, or is there maybe a
> public repo where you push your work ?

This is the latest version, and no, I don't yet have a public repo that I push this stuff to.

I've been travelling a lot the last few weeks, and I'm not quite done yet, so I haven't had time to even follow the mailing list properly, but after that I plan to start a pu-ish (rebasing) integration branch for the patches that I, you, and others post to the list.

> It happens that my refactorings touches virtually everything,
I noticed. :-)
> so there will be conflicts,
:-)
> and the best thing to do is probably that I rebase my work on yours.

Thanks for the vote of confidence. Please go ahead; as I said, you already have the latest version of my series, and it may be a while yet before I have much time to burn on StGIT.

-- 
Karl Hasselström, kha@treskal.com
      www.treskal.com/kalle
Yann Dirson· May 15, 2007, 21:08 UTC · re: Catalin Marinas · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On Tue, May 15, 2007 at 04:56:33PM +0100, Catalin Marinas wrote:
Show 13 quoted lines
> On 06/05/07, Karl Hasselström <kha@treskal.com> wrote:
> >The name of the current patch, if any, is always the last line of
> >patches/<branch>/applied (and there is no current patch if and only if
> >the "applied" file is empty). So use that instead, and stop having to
> >worry about keeping the redundant "current" file up-to-date.
> 
> I applied this patch. Could you also send me a patch for the
> bash-completion script as it uses this file?
> 
> I think the self.__current_file (same for the base file removed in a
> different patch) should still be available in the Series object and
> removed when deleting a branch, otherwise you get a "Series directory
> .. is not empty" exception.

Shouldn't we also migrate to new format as soon as we need to touch a data - in this case, whenever we push/pop ?

Or maybe declare a new "stgit stack format version" ? Currently we have "stg branch --convert", which switches between a "new" and an "old" format which noone probably uses any more. What about versionning the on-disk format, and possibly provide the "convert" functionnality back and forth between one format and the next, with formal documentation about which version works with which stack format ?

Best regards,
-- 
Yann.
Catalin Marinas· May 15, 2007, 21:36 UTC · re: Yann Dirson · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On 15/05/07, Yann Dirson <ydirson@altern.org> wrote:
Show 16 quoted lines
> On Tue, May 15, 2007 at 04:56:33PM +0100, Catalin Marinas wrote:
> > I think the self.__current_file (same for the base file removed in a
> > different patch) should still be available in the Series object and
> > removed when deleting a branch, otherwise you get a "Series directory
> > .. is not empty" exception.
>
> Shouldn't we also migrate to new format as soon as we need to touch a
> data - in this case, whenever we push/pop ?
>
> Or maybe declare a new "stgit stack format version" ?  Currently we
> have "stg branch --convert", which switches between a "new" and an
> "old" format which noone probably uses any more.  What about
> versionning the on-disk format, and possibly provide the "convert"
> functionnality back and forth between one format and the next, with
> formal documentation about which version works with which stack
> format ?

I think it would be useful to have a version file (probably per branch) and just upgrade when a mismatch is detected (in the __init__ function). The other option is to keep ignoring the unused files until the branch is deleted but we might make a change at some point that would break things.

We should write successive convert() functions and keep all of them in case one skips an intermediate version.

-- 
Catalin
Yann Dirson· May 15, 2007, 21:49 UTC · re: Catalin Marinas · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On Tue, May 15, 2007 at 10:36:05PM +0100, Catalin Marinas wrote:
> I think it would be useful to have a version file (probably per
> branch) and just upgrade when a mismatch is detected (in the __init__
> function).

Sounds reasonable, but I'd rather keep that in the config file (eg. branch.<name>.stgit.formatversion).

Best regards,
-- 
Yann.
Karl Hasselström· May 16, 2007, 06:27 UTC · re: Yann Dirson · lore

Re: [StGIT PATCH] Don't use patches/<branch>/current

On 2007-05-15 23:49:52 +0200, Yann Dirson wrote:
Show 8 quoted lines
> On Tue, May 15, 2007 at 10:36:05PM +0100, Catalin Marinas wrote:
>
> > I think it would be useful to have a version file (probably per
> > branch) and just upgrade when a mismatch is detected (in the
> > __init__ function).
>
> Sounds reasonable, but I'd rather keep that in the config file (eg.
> branch.<name>.stgit.formatversion).

I agree that explicit versioning would be a good idea -- doing explicit upgrades at well-defined points is good headache prevention.

And I agree that the config file is a good place to put it.

I'll probably have time to whip up a patch later today. I think I'll call the old "old" format 0, the old "new" format 1, and then use successive integers from then on. I'll make a single version bump for the format changes you've alreay applied, and re-do the format-changing patches you haven't applied yet so that they have version bumping integrated.

-- 
Karl Hasselström, kha@treskal.com
      www.treskal.com/kalle
Karl Hasselström· May 19, 2007, 00:09 UTC · re: Karl Hasselström · lore

[StGIT PATCH 0/5] Metadata format versioning

On 2007-05-16 08:27:11 +0200, Karl Hasselström wrote:
> I'll probably have time to whip up a patch later today.

OK, so this turned out to be a slight miscalculation. But here it is, with a test! The test contains two small tarballs, so I had to whip up binary patch support as well.

> I'll make a single version bump for the format changes you've alreay
> applied, and re-do the format-changing patches you haven't applied
> yet so that they have version bumping integrated.

This contains a single version bump (1 -> 2) for the applied changes, but i haven't updated the DAG patch to use this yet. That's for another day.

-- 
Karl Hasselström, kha@treskal.com
      www.treskal.com/kalle
Karl Hasselström· May 19, 2007, 00:09 UTC · re: Karl Hasselström · lore

[StGIT PATCH 1/5] Fix config caching so that get, set, get works

The config caching was never invalidated or updated, which caused the two gets to always return the same value regardless of the value passed to set.

Signed-off-by: Karl Hasselström <kha@treskal.com>
---
 stgit/config.py |    3 +++
 1 files changed, 3 insertions(+), 0 deletions(-)
Show changes to stgit/config.py +3 −0
diff --git a/stgit/config.py b/stgit/config.py
index 79cd12f..2fd1273 100644
--- a/stgit/config.py
+++ b/stgit/config.py
@@ -99,12 +99,15 @@ class GitConfig:
 
     def rename_section(self, from_name, to_name):
         self.__run('git-repo-config --rename-section', [from_name, to_name])
+        self.__cache.clear()
 
     def set(self, name, value):
         self.__run('git-repo-config', [name, value])
+        self.__cache[name] = value
 
     def unset(self, name):
         self.__run('git-repo-config --unset', [name])
+        self.__cache[name] = None
 
     def sections_matching(self, regexp):
         """Takes a regexp with a single group, matches it against all
Karl Hasselström· May 19, 2007, 00:09 UTC · re: Karl Hasselström · lore

[StGIT PATCH 2/5] Have only a single command in each test_expect_failure

Otherwise, we can't know which one failed.
Signed-off-by: Karl Hasselström <kha@treskal.com>
---
 t/t1000-branch-create.sh |   71 ++++++++++++++++++++++++++++------------------
 1 files changed, 43 insertions(+), 28 deletions(-)
Show changes to t/t1000-branch-create.sh +43 −28
diff --git a/t/t1000-branch-create.sh b/t/t1000-branch-create.sh
index 58209e7..cca5504 100755
--- a/t/t1000-branch-create.sh
+++ b/t/t1000-branch-create.sh
@@ -12,57 +12,72 @@ Exercises the "stg branch" commands.
 
 stg init
 
+test_expect_success \
+    'Create a spurious refs/patches/ entry' '
+    find .git -name foo | xargs rm -rf &&
+    touch .git/refs/patches/foo
+'
+
 test_expect_failure \
-    'Try to create an stgit branch with a spurious refs/patches/ entry' \
-    'find .git -name foo | xargs rm -rf &&
-     touch .git/refs/patches/foo &&
-     stg branch -c foo
+    'Try to create an stgit branch with a spurious refs/patches/ entry' '
+    stg branch -c foo
+'
+
+test_expect_success \
+    'Check that no part of the branch was created' '
+    test "`find .git -name foo | tee /dev/stderr`" = ".git/refs/patches/foo" &&
+    ( grep foo .git/HEAD; test $? = 1 )
 '
 
 test_expect_success \
-    'Check no part of the branch was created' \
-    'test "`find .git -name foo | tee /dev/stderr`" = ".git/refs/patches/foo" &&
-     ( grep foo .git/HEAD; test $? = 1 )
+    'Create a spurious patches/ entry' '
+    find .git -name foo | xargs rm -rf &&
+    touch .git/patches/foo
 '
 
 test_expect_failure \
-    'Try to create an stgit branch with a spurious patches/ entry' \
-    'find .git -name foo | xargs rm -rf &&
-     touch .git/patches/foo &&
-     stg branch -c foo
+    'Try to create an stgit branch with a spurious patches/ entry' '
+    stg branch -c foo
 '
 
 test_expect_success \
-    'Check no part of the branch was created' \
-    'test "`find .git -name foo | tee /dev/stderr`" = ".git/patches/foo" &&
-     ( grep foo .git/HEAD; test $? = 1 )
+    'Check that no part of the branch was created' '
+    test "`find .git -name foo | tee /dev/stderr`" = ".git/patches/foo" &&
+    ( grep foo .git/HEAD; test $? = 1 )
+'
+
+test_expect_success \
+    'Create a git branch' '
+    find .git -name foo | xargs rm -rf &&
+    cp .git/refs/heads/master .git/refs/heads/foo
 '
 
 test_expect_failure \
-    'Try to create an stgit branch with an existing git branch by that name' \
-    'find .git -name foo | xargs rm -rf &&
-     cp .git/refs/heads/master .git/refs/heads/foo &&
-     stg branch -c foo
+    'Try to create an stgit branch with an existing git branch by that name' '
+    stg branch -c foo
 '
 
 test_expect_success \
-    'Check no part of the branch was created' \
-    'test "`find .git -name foo | tee /dev/stderr`" = ".git/refs/heads/foo" &&
-     ( grep foo .git/HEAD; test $? = 1 )
+    'Check that no part of the branch was created' '
+    test "`find .git -name foo | tee /dev/stderr`" = ".git/refs/heads/foo" &&
+    ( grep foo .git/HEAD; test $? = 1 )
 '
 
+test_expect_success \
+    'Create an invalid refs/heads/ entry' '
+    find .git -name foo | xargs rm -rf &&
+    touch .git/refs/heads/foo
+'
 
 test_expect_failure \
-    'Try to create an stgit branch with an invalid refs/heads/ entry' \
-    'find .git -name foo | xargs rm -rf &&
-     touch .git/refs/heads/foo &&
-     stg branch -c foo
+    'Try to create an stgit branch with an invalid refs/heads/ entry' '
+    stg branch -c foo
 '
 
 test_expect_success \
-    'Check no part of the branch was created' \
-    'test "`find .git -name foo | tee /dev/stderr`" = ".git/refs/heads/foo" &&
-     ( grep foo .git/HEAD; test $? = 1 )
+    'Check that no part of the branch was created' '
+    test "`find .git -name foo | tee /dev/stderr`" = ".git/refs/heads/foo" &&
+    ( grep foo .git/HEAD; test $? = 1 )
 '
 
 test_done
Karl Hasselström· May 19, 2007, 00:10 UTC · re: Karl Hasselström · lore

[StGIT PATCH 3/5] Upgrade old StGIT branches to new-format metadata

There used to be a "stg branch --convert" command that switched between "old" and "new" format metadata. But my recent metadata cleanup patches introduced a "new new" format, and more are hopefully on the way, so it was time to start versioning the metadata format explicitly in order to avoid future headaches.

This patch removes the "stg branch --convert" command, and makes StGIT automatically upgrade older formats to the latest format. It stores the format (as an integer) in the config file. The current metadata format version number is 2 (the "old" format is 0, and the "new" format is 1).

Signed-off-by: Karl Hasselström <kha@treskal.com>
---
 Documentation/stg-branch.txt |    4 -
 stgit/commands/branch.py     |   11 ---
 stgit/stack.py               |  153 ++++++++++++++++++++++++------------------
 3 files changed, 86 insertions(+), 82 deletions(-)
Show changes to 3 files +86 −82

Documentation/stg-branch.txt, stgit/commands/branch.py, stgit/stack.py

diff --git a/Documentation/stg-branch.txt b/Documentation/stg-branch.txt
index 25c9c19..25ca951 100644
--- a/Documentation/stg-branch.txt
+++ b/Documentation/stg-branch.txt
@@ -20,7 +20,6 @@ SYNOPSIS
 'stg' branch --protect [<branch>]
 'stg' branch --unprotect [<branch>]
 'stg' branch --delete [--force] <branch>
-'stg' branch --convert
 
 DESCRIPTION
 -----------
@@ -91,9 +90,6 @@ the "master" branch if it exists.
 Branch "master" is treated specially (see bug #8732), in that only the
 StGIT metadata are removed, the GIT branch itself is not destroyed.
 
-'stg' branch --convert::
-	Switch current stack between old and new format.
-
 OPTIONS
 -------
 
diff --git a/stgit/commands/branch.py b/stgit/commands/branch.py
index b043c69..5e7b0df 100644
--- a/stgit/commands/branch.py
+++ b/stgit/commands/branch.py
@@ -45,9 +45,6 @@ options = [make_option('-c', '--create',
            make_option('--clone',
                        help = 'clone the contents of the current branch',
                        action = 'store_true'),
-           make_option('--convert',
-                       help = 'switch between old and new format branches',
-                       action = 'store_true'),
            make_option('--delete',
                        help = 'delete an existing development branch',
                        action = 'store_true'),
@@ -186,14 +183,6 @@ def func(parser, options, args):
 
         return
 
-    elif options.convert:
-
-        if len(args) != 0:
-            parser.error('incorrect number of arguments')
-
-        crt_series.convert()
-        return
-
     elif options.delete:
 
         if len(args) != 1:
diff --git a/stgit/stack.py b/stgit/stack.py
index d9c4b99..223f3ee 100644
--- a/stgit/stack.py
+++ b/stgit/stack.py
@@ -273,6 +273,79 @@ class Patch(StgitObject):
         self._set_field('log', value)
         self.__update_log_ref(value)
 
+# The current StGIT metadata format version.
+FORMAT_VERSION = 2
+
+def format_version_key(branch):
+    return 'branch.%s.stgitformatversion' % branch
+
+def update_to_current_format_version(branch, git_dir):
+    """Update a potentially older StGIT directory structure to the
+    latest version. Note: This function should depend as little as
+    possible on external functions that may change during a format
+    version bump, since it must remain able to process older formats."""
+
+    branch_dir = os.path.join(git_dir, 'patches', branch)
+    def get_format_version():
+        """Return the integer format version number, or None if the
+        branch doesn't have any StGIT metadata at all, of any version."""
+        fv = config.get(format_version_key(branch))
+        if fv:
+            # Great, there's an explicitly recorded format version
+            # number, which means that the branch is initialized and
+            # of that exact version.
+            return int(fv)
+        elif os.path.isdir(os.path.join(branch_dir, 'patches')):
+            # There's a .git/patches/<branch>/patches dirctory, which
+            # means this is an initialized version 1 branch.
+            return 1
+        elif os.path.isdir(branch_dir):
+            # There's a .git/patches/<branch> directory, which means
+            # this is an initialized version 0 branch.
+            return 0
+        else:
+            # The branch doesn't seem to be initialized at all.
+            return None
+    def set_format_version(v):
+        config.set(format_version_key(branch), '%d' % v)
+    def mkdir(d):
+        if not os.path.isdir(d):
+            os.makedirs(d)
+    def rm(f):
+        if os.path.exists(f):
+            os.remove(f)
+
+    # Update 0 -> 1.
+    if get_format_version() == 0:
+        mkdir(os.path.join(branch_dir, 'trash'))
+        patch_dir = os.path.join(branch_dir, 'patches')
+        mkdir(patch_dir)
+        refs_dir = os.path.join(git_dir, 'refs', 'patches', branch)
+        mkdir(refs_dir)
+        for patch in (file(os.path.join(branch_dir, 'unapplied')).readlines()
+                      + file(os.path.join(branch_dir, 'applied')).readlines()):
+            patch = patch.strip()
+            os.rename(os.path.join(branch_dir, patch),
+                      os.path.join(patch_dir, patch))
+            Patch(patch, patch_dir, refs_dir).update_top_ref()
+        set_format_version(1)
+
+    # Update 1 -> 2.
+    if get_format_version() == 1:
+        desc_file = os.path.join(branch_dir, 'description')
+        if os.path.isfile(desc_file):
+            desc = read_string(desc_file)
+            if desc:
+                config.set('branch.%s.description' % branch, desc)
+            rm(desc_file)
+        rm(os.path.join(branch_dir, 'current'))
+        rm(os.path.join(git_dir, 'refs', 'bases', branch))
+        set_format_version(2)
+
+    # Make sure we're at the latest version.
+    if not get_format_version() in [None, FORMAT_VERSION]:
+        raise StackException('Branch %s is at format version %d, expected %d'
+                             % (branch, get_format_version(), FORMAT_VERSION))
 
 class Series(StgitObject):
     """Class including the operations on series
@@ -290,6 +363,11 @@ class Series(StgitObject):
             raise StackException, 'GIT tree not initialised: %s' % ex
 
         self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
+
+        # Update the branch to the latest format version if it is
+        # initialized, but don't touch it if it isn't.
+        update_to_current_format_version(self.__name, self.__base_dir)
+
         self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
                                        self.__name)
 
@@ -299,19 +377,9 @@ class Series(StgitObject):
 
         # where this series keeps its patches
         self.__patch_dir = os.path.join(self._dir(), 'patches')
-        if not os.path.isdir(self.__patch_dir):
-            self.__patch_dir = self._dir()
-
-        # if no __refs_dir, create and populate it (upgrade old repositories)
-        if self.is_initialised() and not os.path.isdir(self.__refs_dir):
-            os.makedirs(self.__refs_dir)
-            for patch in self.get_applied() + self.get_unapplied():
-                self.get_patch(patch).update_top_ref()
 
         # trash directory
         self.__trash_dir = os.path.join(self._dir(), 'trash')
-        if self.is_initialised() and not os.path.isdir(self.__trash_dir):
-            os.makedirs(self.__trash_dir)
 
     def __patch_name_valid(self, name):
         """Raise an exception if the patch name is not valid.
@@ -410,19 +478,13 @@ class Series(StgitObject):
         return 'branch.%s.description' % self.get_branch()
 
     def get_description(self):
-        # Fall back to the .git/patches/<branch>/description file if
-        # the config variable is unset.
-        return (config.get(self.__branch_descr())
-                or self._get_field('description') or '')
+        return config.get(self.__branch_descr())
 
     def set_description(self, line):
         if line:
             config.set(self.__branch_descr(), line)
         else:
             config.unset(self.__branch_descr())
-        # Delete the old .git/patches/<branch>/description file if it
-        # exists.
-        self._set_field('description', None)
 
     def get_parent_remote(self):
         value = config.get('branch.%s.remote' % self.__name)
@@ -503,15 +565,16 @@ class Series(StgitObject):
     def is_initialised(self):
         """Checks if series is already initialised
         """
-        return os.path.isdir(self.__patch_dir)
+        return bool(config.get(format_version_key(self.get_branch())))
 
     def init(self, create_at=False, parent_remote=None, parent_branch=None):
         """Initialises the stgit series
         """
-        if os.path.exists(self.__patch_dir):
-            raise StackException, self.__patch_dir + ' already exists'
-        if os.path.exists(self.__refs_dir):
-            raise StackException, self.__refs_dir + ' already exists'
+        if self.is_initialised():
+            raise StackException, '%s already initialized' % self.get_branch()
+        for d in [self._dir(), self.__refs_dir]:
+            if os.path.exists(d):
+                raise StackException, '%s already exists' % d
 
         if (create_at!=False):
             git.create_branch(self.__name, create_at)
@@ -522,45 +585,10 @@ class Series(StgitObject):
 
         self.create_empty_field('applied')
         self.create_empty_field('unapplied')
-        os.makedirs(os.path.join(self._dir(), 'patches'))
         os.makedirs(self.__refs_dir)
         self._set_field('orig-base', git.get_head())
 
-    def convert(self):
-        """Either convert to use a separate patch directory, or
-        unconvert to place the patches in the same directory with
-        series control files
-        """
-        if self.__patch_dir == self._dir():
-            print 'Converting old-style to new-style...',
-            sys.stdout.flush()
-
-            self.__patch_dir = os.path.join(self._dir(), 'patches')
-            os.makedirs(self.__patch_dir)
-
-            for p in self.get_applied() + self.get_unapplied():
-                src = os.path.join(self._dir(), p)
-                dest = os.path.join(self.__patch_dir, p)
-                os.rename(src, dest)
-
-            print 'done'
-
-        else:
-            print 'Converting new-style to old-style...',
-            sys.stdout.flush()
-
-            for p in self.get_applied() + self.get_unapplied():
-                src = os.path.join(self.__patch_dir, p)
-                dest = os.path.join(self._dir(), p)
-                os.rename(src, dest)
-
-            if not os.listdir(self.__patch_dir):
-                os.rmdir(self.__patch_dir)
-                print 'done'
-            else:
-                print 'Patch directory %s is not empty.' % self.__patch_dir
-
-            self.__patch_dir = self._dir()
+        config.set(format_version_key(self.get_branch()), str(FORMAT_VERSION))
 
     def rename(self, to_name):
         """Renames a series
@@ -666,15 +694,6 @@ class Series(StgitObject):
             if os.path.exists(self._dir()+'/orig-base'):
                 os.remove(self._dir()+'/orig-base')
 
-            # Remove obsolete files that StGIT no longer uses, but
-            # that might still be around if this is an old repository.
-            for obsolete in ([os.path.join(self._dir(), fn)
-                              for fn in ['current', 'description']]
-                             + [os.path.join(self.__base_dir,
-                                             'refs', 'bases', self.__name)]):
-                if os.path.exists(obsolete):
-                    os.remove(obsolete)
-
             if not os.listdir(self.__patch_dir):
                 os.rmdir(self.__patch_dir)
             else:
Karl Hasselström· May 19, 2007, 00:10 UTC · re: Karl Hasselström · lore

[StGIT PATCH 4/5] Test the format version upgrade code

This test contains tarballs of repositories created with older versions of StGIT. It also contains the script used to generate them, but at some point we will lose the ability to easily run old versions -- for example, if git changes incompatibly -- so tarballs will be the only practical option for sufficiently old versions.

Signed-off-by: Karl Hasselström <kha@treskal.com>
---
 t/t4000-upgrade.sh           |   40 ++++++++++++++++++++++++
 t/t4000-upgrade/.gitignore   |    4 ++
 t/t4000-upgrade/0.12.tar.gz  |  Bin
 t/t4000-upgrade/0.8.tar.gz   |  Bin
 t/t4000-upgrade/make-repo.sh |   71 ++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 115 insertions(+), 0 deletions(-)
Show changes to 5 files +115 −0

t/t4000-upgrade.sh, t/t4000-upgrade/.gitignore, t/t4000-upgrade/0.12.tar.gz, t/t4000-upgrade/0.8.tar.gz, t/t4000-upgrade/make-repo.sh

diff --git a/t/t4000-upgrade.sh b/t/t4000-upgrade.sh
new file mode 100755
index 0000000..8a308fb
--- /dev/null
+++ b/t/t4000-upgrade.sh
@@ -0,0 +1,40 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Karl Hasselström
+#
+
+test_description='Make sure that we can use old StGIT repositories'
+
+. ./test-lib.sh
+
+for ver in 0.12 0.8; do
+
+    tar zxf ../t4000-upgrade/$ver.tar.gz
+    cd $ver
+
+    test_expect_success \
+        "v$ver: Check the list of applied and unapplied patches" '
+        [ "$(echo $(stg applied))" = "p0 p1 p2" ] &&
+        [ "$(echo $(stg unapplied))" = "p3 p4" ]
+    '
+
+    test_expect_success \
+        "v$ver: Make sure the 'description' file is no longer there" '
+        [ ! -e .git/patches/master/description ] &&
+        [ "$(echo $(git config branch.master.description))" = "cool branch" ]
+    '
+
+    test_expect_success \
+        "v$ver: Make sure the 'current' file is no longer there" '
+        [ ! -e .git/patches/master/current ]
+    '
+
+    test_expect_success \
+        "v$ver: Make sure the base ref is no longer there" '
+        ! git show-ref --verify --quiet refs/bases/master
+    '
+
+    cd ..
+done
+
+test_done
diff --git a/t/t4000-upgrade/.gitignore b/t/t4000-upgrade/.gitignore
new file mode 100644
index 0000000..d412eb6
--- /dev/null
+++ b/t/t4000-upgrade/.gitignore
@@ -0,0 +1,4 @@
+/stgit-0.8
+/stgit-0.12
+/0.8
+/0.12
diff --git a/t/t4000-upgrade/0.12.tar.gz b/t/t4000-upgrade/0.12.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..b183fcb1983eaa11c1a2eba3f0bb47b5c1c845c8
GIT binary patch
literal 11077
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
Hi12>^a3NeI

literal 0
Hc$@<O00001

diff --git a/t/t4000-upgrade/0.8.tar.gz b/t/t4000-upgrade/0.8.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..7a7d3c2f8d916dc79c55f0f0b2426005224aca97
GIT binary patch
literal 8224
zc$@(sAm85~iwFS1Elx%N1MFN2lpED~cFLpGKu-enP)bR-)(Wq*)<_yX_rb2cegMY!
zQ9pRtyIzk*ccke`Gh$}6Yhf=<z=RY?Q)tts#n3>Sl7vE%HZ(v_9+({3gcj4&LLn!I
zK-v^eo0HQMl2Xz~`v3Rdkw((4*CxAi3cAN@jb`rs?|=W-{qBG7C=nZsuUO`ifRf8(
z@KecU68=xGtWXl!WGbD^<`PM`pG!k^l38xXu+$t^wFy}<T~d!*Z?Wh9PfG&yA1fN}
zavH%&>t9Kvlx$D`-6#$H+f;X!$p}tH|Jhs)^sgjyiJtzuQCjt1qH1C3t>EPSA1;-i
z{<~4;(|<*ETxu_2A5gOC^uqejC~f*rWiqK1B(ZcXEL`5<`lpJTNy2=qr_+OJHJ7A=
z5QEdopq|t630lbH@&#HL(lqH*GukW5Qd<4rRg2320#0uJ&tm*fCVTk5E2UNcdDWq(
zcK+x1FPX~r^xu^-pZ-tf{Ll5D%D_i&{_jFb<Ur~PT}>;gLYfW@Wpe3sLZhmhE)3@N
zp&UL_b5!Zg|8LLI@c+E6np$ZYqW@c({}rVd|GQEe`Y&15^fD!YPG0{?8rOe4{V#uM
z)4y7+mg`m3)k<>3DK1qXi_HJooYJQMbSl+b|97SIt&HalGwzh6KB<q4n}i-vE7dY3
zXb;5EY@_ND*CH@AXw$?kQKDIuis6#7RU{SaIBJm+SDmIN$=88G(}0L>TNR=LMY*CH
zWv_CKb+oJG^-!{E<pT5yBxen`L`=(+Z=kkCKre31A*v2olo*avGt458<9*mx)izX@
zHdrupf)rHKb&y@hwW>aDW2D)m;UI}}*_x#VtV#>e&^IuTqHdHuW}%X)=0PZK$#58z
zD5=1h`FE@c1E5;X#Y!<riV^TA*(u2mb!%10rH)JF10)!_eABL}4V!idXX72BqXo&T
zRZ;+H9Jf;Fpt3p`S{s~`hL;MDwfTRuw<V&_`Z&J-D`}YQTm3(k?&bfwQ<lR2!Ar<`
z@F?)7e4UxTsKXrEGAU8*Vy!|=H_9lOYK0O@XV>UF(A+JlE-9(bJdWu1-36lskXRsI
zoJ_O;D)YsQWsVkoWVeabKt7Ceo<eWfms%(gmFzT%CN0R8uFD|ZvH{|RXIRlE{+-*_
zjfTP?dNKpnj1gpf_pU3p?U>rMVdK_a<PdRyec2@aPJG`)I5sdg5s3|i;w`j?$%GQG
z_D6tR(WX@*U+<9Mz7UWOLR3<M<j^5PwUPy2*uAkac3WcM97g6@j)u{nI;2pmmJJQX
z-Z=yZ#?8WFFi1v$Oqli9zpss<e$lZMAdY3bFpipOTu)@p8xbY_@qK$qU~)jg|Ko-J
z2;=i0L{nsSY*mtMI6fZ%W=)DTC%qHbxNY&jYB`?wE@2;w;J;+59sj4WdN2NWp`2L!
z$LI_muJItxL##@inx;Vr(re{L;KW)LweVI$;V{{1!6UQyPISdnQs(pjTD1VtatZrb
z#Q#%DuIK-!sGKPNtHS)O+JNLpRn?{;2*7vCF<i^86Wy{&%>hU^Lv0{Zs8#aBwN=xp
z!kq2Al~8~<GN_d*R)MTOKzv3Sgdo9|Nr*D}Em&mP(6MZoy44FCCtKS6-==bN3BOc*
zESCRCq&xCI$=?1?SIUX{KX{m-*U79v0&3o>vHN*%CFIfq5CFgZ8O;DKYbFQXhWB%v
zOpd>jKa+DxgVsF$ztr%5G5=2`(;fIfol$!J-;J`A{(t;DkZCp2Ku%{)2b!LE1X#fT
zfyUBD0Fm5)e@@vbmfUik04y<d14hZ>+qhY<=`7g-SjRH^9kQL;I@MgV9YY|B<SNWF
zkh)bPhUwZ?p{7wU=$RH~y=ExFC8Lakn=Kg-WB_Baw9a_h@ltOHvS8$fSpWg5u3?$Y
zq}k5xpa-ac@NC!kI^Y_DA<EyTGcv$7z-F>6G<e0v@eL$77BA45xLGTgC6JIQ1+X@Z
zN|&*N14OP8c?&esROr3H8cDS5P$|$ywr<?GVdpj5c5EIdvyG8d#lTd8RJa;3@>i;E
z9Y#k%+0<sufi-A`T0$iicq+@mlpx~}Gc`Ck94|gGOv5$QGBGNMCSy`ySkeu0NUBo1
z%<?}Hfl{fHkZ#q?!c<-@kWrEtY2F5vV#cv~2cfGC_B!P~!W2}n`5Y5Nrp3QIwLAfm
zQ!MXxPzu2FaA>wvkMe2}Xu5W4L<+#D!Yi9BM6fQl2P1`n?%cIu<BlC9I51&CKl42x
z*MWU7Q*bO`$f-G1Lo+Neem<45WYzG3&6^Ke#Gt^<InLZXqlOVRq_YsiWRuyf<m+WV
z8Tv$xy|z<Av5_znq!4k06Kt*6pfW6K@lRG2yPjkJgP(w>hA$msjN<?+hxReklVwbU
zs_}4aAVTu-2_<x>F}RSQTSbh;qzBxloZ|b|t|jus&eddMb$Ft%IudWT=KQd>L?#Qh
zCmk(Tub#Im=3-aJ7p|?9tV&f}Gtv`Ih}RByCK(RS6FGuloKq3eaL(6$--I(j0v#lH
zOu;CkTO!kqI1WR}!lc4$hWB1NHaZzOn49aMSTk+vSmha@RH1eeL$Ilr4b5$*H|J9+
z#{aO-g+`?QcVtSot7RViXHz@|xRiYy7yl`VWIEe{|B~4r{_93*#eeIqYTZVZL&92w
zBtdQD%9?3dWIee;t*E92wWMk-UpAc5C<ZeLO@$vF8)ch85FGfvPN)xec9H^K02DK{
z(V*`olTb1!5Cwm>AEUNYujDO=6JUcn10DNevX>+cIsuB!zM>)4C<y34;-^)kKGJK}
z#0Mf$U}QwX4+bj}WTX#a2fz))Trl5I+pYn$wSYexcY&ZbbyN)j-&SiD>tedAKvf=G
z8J;1mSHgJ!$c0b1w;+B+L<We{(g|k?K<*|75$U$gh;(>G_`U}F*yIReI$*FmkCJSI
zk+)qV0h?2XImN#+=`b`0l7|(j41Vgm^-;^DY(Cw`tAUKTT=qBgAm0EtTWAG^N*eoP
zq7g#e`I#Akp}I)krWI=jhY$;T+h#(p8UulBpA;TPNxsJRGC_hs6{gIp1tO0zW%|ZW
z0rB~ILxWlaLel{QF}&<U$*|5~0z{D`mbc7ueF_^)DQ&`O$YP&oB%y=CWan_(5MaxG
zU-RSw&6{PO=1nX2@xga8eUv>(3-LGYyxCimN#5;#h73A1$LoNrFnaT<PA%JD{B=^R
z%C042Vw4-Lr}V&EQdoeysBj`F01D)H1Y<bRMUB%Zz@~u)ngBk4s9KB!u+{jE%dg&>
zm>d!JD^3vwJK)v81rJ}*H3!|aA=Z=Uq5>~7(0+-JV?^y0waP$C0HDm?ggm6m`sC^e
ziDUgh7y#br7*P<Z1m=iSEdw-(PnhwjU{9H3k8qKKJkm0)00Q+$w$Vf+8sdZldpYG$
zl8tX(+R4cX$5VV!0it(cs39N!2U&;m7)WVOROJ{SWaWKtMb-F(GY|lH1;Mp>V@~R*
z9VA`{%A^n>s|J7l4HY)66Ilv&h8b^T9i3j-kJR^57D-~OamSydP<2|Ysdm9>3D4B=
z*2gn=qyqd#7@@ar_rmuq0?rr`29L4qX&qfcb(~rSY6_1SmLxJjwwY{!+cF7&1x!1f
zU{DM^+PGrAgh3ARt*}uLvxSp_T2u{_gsFjE2a9H#YM5I9g911%zC~gS_S*>78pr_m
zWX(cL4-ICpYrZdA0G(o%hDBHmuO{U_ldl;N3!2lH!B6~HHgbXOscM%E4Ao$X>NItt
zz>P^~J2=w78(qG7K)JAF4I_XHdDp0VhkiWfSQ>~YS1`squ5B<Om=yPwRlCmOk{AId
zY1nj1VHN;pfW>J_t2h#y02Q_s<rrufTx{1suw*lgf2=IDf{=wV@YY&MHH(xbjo6T|
zN6(6)Vg&<ySA=M}z4HpfaCo9DDvU~Pq|u^ae|AB$3X~sh^12l=!=<RivuK<u55!zw
zAdMN!e6V?V8mC>hj53Nfn<cQ|q0~q*^C!S7j>T{)z}Gbx{#jPbGwUc5#ViZ`UIJ#?
zv}VD)!%Bq2VliCYwOJa*TP8+yB44EC41)@<0|zRy6<nMDX&?ul(r{~4qtGxP9~ZWY
z=Fb{yWeW9cRWFq#L<RDUGV5mN0{l+RVF5TcY4a|I=~~$U0i)S~Z3(=_CP#}K7g2T0
z*rDcl(<-&liU?T;%COM;xJDWg#3x8b09o+_3K0c7n1K{2oIIXXnLyhiSHm|LVX!Te
z^@W4r4oukbv@Iwio)d$9%k^j*U}CN3aco&`86#L$y)lc55pZoVm+*vc(=u~xaqx_f
zL>0Ly5HSm8;8^HQta&pQ0+cg+;YP37tb5^dQ6X6YFh%DWJbWF60V<T#s>1<IBVEYT
zR;q4OM{G6W5k6*zJk&HrDP2X~eutECzEEB66v9Kn_5+xWuw+?Y^iLQ)YY`Xl(EIGT
z##(qog_ii2C*$*0inraHr;hI~{%Jt1@kiEm){Oi9nq{J+qQ73AvADjarl=Pu$0r$Q
zQa=#^HU;US1W6i?!Vu8I*Rv&8YY^d~e4P0Uo+Q<c0}$keOU}Yg;W-dDnlXvBS;yMw
zhRHk@XFYcTuM=&(NgG)a(HO^tUk!8tbP*n_y_uBtvz}4K2+$rI(bNE8V<@I=QI`Yn
z^@5&=Nm01a@gT)Gk9gDsly0j<Dy{42co$C4m1+&*0fcTe;(1pw`?xgOQhoj!qOcl#
z@c~lDxB#9VB~8ff8zA5otlk`+pP6JQGh#U$<8!O%girfyf4~a@3x~o}oh5numbU!g
zQXl=|eJry7n@zQ!|Hsc>{;wP5ME8I5WlMvFPR&$Jhy)xAs70~zp_onspwPJi3@Zp~
z%upE*jfo_hw+B3o(T?rKI3-n)Er^cn0u~lO^@*{@VKJW6L}&~bGAnSwY(2(eux~){
zK&(gdmc_uRLt)`PTcQlHbPRpgD3>uPdv6CY?8)CEAim>q2qWtdxplQxW;qrBT9(<5
z*cJKXd;yb{3dVfQm`Fk}JfjMNfk+D1$)E(X;n%64*Kj8+f89FYB*whdUY<A->w+Me
z93j4ZUvLBK+OC;gibBC2dSZzckBwLmb4*@C5*9EHr!bB-c@D{`;ej6_I%J(8{no}L
zlpvSFu*S%xd^~W$2QwD&YBsY4&0z>h1PMR}c7wNWe&<9whPfM$1PmXi*UnBoJuqWw
zw+;Ssg^XI2#Zn8<5}3&XwiH2n&5e800t4W(HY`)B+Saebf&*^>Vedr-rP}X1Gvuou
zjQAwtYs;k2c4rjEZlcKiCp1UA*H%2g!DK6X9^6FsU}&>6-yy_-fdR6e6Zf;vNdP9`
zMogKXEGSR@-cY?t@wGb`hR_(!_Za+Pb%Y-X0RW(@-Z64K8NgRNs(^w%5gg<s`uY+B
zb3Uf|-6`yAl#}stmA(gDkDBVrY!tHVF%<FdG<aqg^$d(PX4?&c<`3lzM0qz60P%eg
zALNz*0XJbeX1i0KH-`?2tG>P_4NR-Ckp)BTHWVZ)L7T2!qrO1~e7eEJLwm1L5^Cu+
z>Lpj2QHt9^(>jFe{FHZt6#8*P1}LJ`RdR^P4w>xd$1*Vbd7*$G4z{YW1auoA!%H?}
ziqwGrMn+oY1|M8#?GkHL<U2X75*CKwtA!g9E^V*p+A!PTYQCkx0Dm}c*4d(#plv`<
zb41!(_1|9;ky^yTrW5As!ys`?nV8I`ah1vE8ou(^iDx=2)rB=$4TwvpAspYP6FSRf
zVzJ_-OS>@t&v*V`OxjD=$0Fx{bGi2O|LKI%+yCuKIU)S#oyGE72j&D#R~+SSA_<Hv
z05156y%zL`i%3<|XcfJkzeXVPdWe{OuM;#D>D;r#(jC)ps)KW713~gwW?Qvli8yY-
zz>~r37DbLLh{HPOeTE0nLC4_nWiYNr8VG8uYC4ljD%paPPto)c&FNWvXfT~tbTyM5
zN@oiNt&mld36dSub822sQ+S?C4kmJggF|{Inb#8O6x9kTC7&D82bDDG_*F$FFo&`e
zF|KXoh2MF{`y7!q#y2WjUJ7LkZnm8kwlEWzH&io%@KNfEk$BCqL4~SK;{{8L^OKkG
zrL_;g_GF4DvL&hM|M>dE&%p1$^51_{l9}G`e|Dv`>YpCa%C$n5*Z+xBw&VAIvZ-GD
z?@H-o%k;9t;IIQIgdd233t<Z~t_?>!Or&wHneE4*48_^){=R;e#udQ}U;kopRwZJc
zLMjl4V8IBHdVN#`Ys9APm<?M|;+mQiEm+FK!UBL4I~&e=#SI4tqNYQ_E#a|3e2Y_r
zVwgq6`w@)m1F^l9I*AuI_A-fWrPcp&x>|-l7V&>2n{EI7XQk)=-6<#f`+tFs--nb=
z^rwRZognB`kGfa9;`{wizLZ{$K28GviTNJ_Mi2jWqcrsIOYkjuAB)8QL@wE`|4c5^
zi~rpyZ#7xm+mn5Jl!pIXc?9RDJpPOIe=eQt#s6-U4*g?ZZi$WH<n<5pK=1c|yHe)Y
zKmI2dlx$8N)Dnq&MoDK?RaervT!Lyk)v~mZ9ZJ!3K~Lqi7}h>+A5hW@&;QAEHrJ~E
zWHO!T{r*o^%J%W~o8jZ<;P2eKfAPSPTkd#TI+}RT#y8%)?Zvb1`s3fZ{Jsx;XODZ|
z#ff{ierkUuaL#`p>O13$FFp6lS0B6N_7%Z1`wky{D!+O7bo+g`{?d<EuKKHIa?htO
zdGP#i{2)p`cqH@qrVpJHx%M}oKbTg|`2HhjKRlEC!r$Dsj~+=~^zxbi7<V6h;pw~o
z<hv{P-uu9NFF0>cQ5${#hacYg+}1aCgq}R-nd+X;|JRFa9(jEGE9YGJqjx>uzxE-<
z-tB(&Hobm0c-!>f-E!ksL-%Zd;)QdA|M}+ClKG_vpM2$8SO38iJAUhhPhNECraR94
z?#tm3?Zq2DdhM-u9Eq+zKehYD?6ZHmCA9hGnXg}POY##Rd8wE={K@aXXUq0Go;vG>
zN3Q=&_5Hs(@s(#kn?Gg^yngc+je(K1?h|1vbM&&<dAG}NJpbrX#@$a|Tw`5--|t=a
z>~~K4;h(R)nQ`|t<L;JM=I+h^$4w#Q;q4o)mmh!j=Ra0hwd-?#arhU;zWmXf_8$A!
zS8x9Nfz+q+o6dWB|4sKlbK%9>=f62}%U!1pNgta`|8@SUZyY;wC~~)Z_SfHxZN6*c
zg@1PE_EpD}$JYJBV>@>B|NC<v{PfXxnbrTf>?=p_{r2E9?|$iDjBkBl$Jn0t9=Y>#
zcRu~K`~T@nnTM-~Q%9cv-f#T)RXO&;4_53swBjcZtbMJMzpMT*{mbF|A3cBawbQOz
zwR7Owz3WcD_}f=L@lf$cuRrnWji+Dq_;2r=Idb-ezkkNT@;w(^e&nHJAKnr<yeji+
zf4l3SuUq-?3$Fa$uiR{G`NC`3*<btSo_p>X-}a*OyI;QMho5<J*UKw%kKXu+y=vUL
z@YP$(^$TY|{>Vov=N;Mj!pq4=UOVvF-O@#`oc%!NFaPL&>kpo_`+aL4eD|?`TRX$G
zJI=NHf9;(KR1`-V$9q5phSLKtvZjrR#2~)D=E7SbapD5&y=qLDqX)+TGXWI5uWVN0
zfk|8;Q84kLW>pl4;JF*cg~+kG-Wo5^9LA96u_kQrV7qByp99R{&Q#~Y_j}Zvc}-7M
zLCycG`l_qyQ<c<oREMd9C7nwOZ;tXOqJ;lnpKI%uPOHClx6hZ|JCE=gP!v_(<xs#&
z(P5qE_89!<tZZ7Zz2)}T^c9C*nKA0p&B}AJ1znc*dwW(l!}|G|?xCyHE3c>i^p)Fd
z`;I0ox$=tNy=u+ea{20`eO7)})%%)u@v5AyhQUK-{>3eL-{CnWS59!X3psyb*`&9A
zrA8e6(3`K_5vg0>ZrJUGB?mpK$M4;mKQMBu-^tCxM%-5yfBjz1`@bf5PG5hTAo8eX
z>EY60oxa_3zO<k)eYti`blJPdx_PZSf<N4TB~$9L@5s*~I{$eKAKcq8Cukidd13#^
zi`hl_AB1T0UfNq4xZ1oV`TX6<^_Q>TKNk9+R~<g=>bLTqyCi8FXof#6ZS3fZ6LDE)
zL&w^cg<U$G-txxOx-peMZe&jH3BYy~`w)k?JO5o)Ic6eJbx$((y?}DhQN5BSSn?W=
z42w)WFU>i1BdS~dHxE{H?-iv~J-YAy!^7WUMFGCYzi?}wN%+fM2VuK2yk=QsbAg#2
zr=DWWKE7p4YHr-T`Iqw7nGOuccJ251zR<ER%e}Et=Q?h)%;Da~Y0PJf-u(PbP|5Au
zm`8tmb1yx*7q)wf@0wvxW$*^}{8aZ+i%%=JBrdvIdV6f~N#C7+5AHT;(Zj+|;=HgO
z|L`5GX!^7p<`pQpVe!c{dF{!Xs2;mi^HSg58Ip0}otx^II}upvPktMSv<>dOg_LX?
zncx4T-Dk^^rWa0lIQz%{9R6f-Z*h&b6LCiWZ`7w*FT+Ovub>Ib{-0vt{O8{S5*H^o
zCnt9&qz-nZj$G<wBYD`FJnc+gE=tn>GkGIQJ~oo?ljP^5<ljOHa8c^qLh6Dhxd(eN
zc-_dM(H~Ea@VS+H;mb_CZfiC7HJ3j>J+$iGC8sCM**I=?Z@*Ou@0YIK$z3m^vr9uC
zXMMjQ&o}u`72}6b?D5>COfH{o5wG>ht?8ovG<CE7+Pn)tm+1PJ-{bDT6ZKa4<pE*L
z>gBbmm%Z@2eHOeE@f_b~(UAvco|!+fuYb>d8KF69GVsIdKXUi?nzX-cbj?)%kFIUn
zM6D^ytf&ey?c=Ve#m=~Nu7Z$X_3xM{-*INaiwBp74B*PWJ@Y}=6Iq`INqpzlPWo=?
zarNDr#6g@nNMC$o`&WmTjbH2W@%u+2f@qJt`8oC6{b$xxe(s%>*OOSc_SELO?yo-%
za$7fntIzgrJ33U(SZrK`UCa)tsm6MgJT5xRl^a`-IWTwjtc3#$PF)UKKl+#aJip8s
z?)s)Z_1=N8895Vs*YvHuR{rSx!u{j=ao4L#gNL6O5`8_kBtJ6h7{2{(-FBZ`&fNV_
z5m=B_lzjMKccP|z?Y(fl>59C*D|cO8y}7UZ{n4){-R!ULSzF<EcHY&Cm7Iy`{ayC;
z^#v!-g^yV=U}x&o3&r289REQ}gOMpO)^4uw2#7hX&OTs_@QlJG|K#$uk7oV93)TTW
z!~75b0`orzXu19Q@t^Y+0@!^1)9m>_%?}A-{D*));EvDr!KqwrhbH~=r!Bdv3|soA
zEZ2W?-39o6NJwH0VJ)mu7Q0#iBAWCs>}qpO8MfB{(U#}GlQP)<fre@PMuF-4cB;np
zAN<w{P0DOGDVg8vH&i(FE1pd->G@Mr`R!;@FD<{qcPMVqH0-a7H^ph<<N31-8a8Ys
zH{K@p-v-#GN&iodauIv|hY<gpRLBYN|4`7Z{e=O5Q@;j|`Tr#E|4}ro|3N})?axQ?
zFtU2*lwoW9Pm&h?KO<8>{0AhM38|Tsnkne4+b6B*e^#RNEPze@6PELTGLnJ$KO~rG
zshI)sw_94%zcaUk*zW%si}AmVGXS7}6zKRfjGmm6B6eK>^t7gbGvPcJ&~g8tmMP%*
zFA}V@zt)tJVoGpE8Mf@djI^}>+z;4)6xh4}8#;%!i52~)n9NQp!(sh1Gzrgtk)R}4
zTBFq|bcB+Y(JF>es8mXgj${~8OVflxqgUuutib^KZ<|=rztdbm+w(sP3;oLp8v1`E
zG(@RI^PdKOgS4i9-K?Y}Hqkjzz-{ZFwtW5r=f5EWp8T|LTJ8Tyapr#%+<SihM+WCV
zA;C)fi!%Qsw7-nI1MEKv!2fICSkb>I^FM<A8A<{1e~_Tlk}@r&QflQ2T1V&!1HtHJ
zD!qo#Dp-|PZqVy#xxqlvp#Qds75$4c|0C#MPEgSQBSDz|A;qo+AQG+WpAy;sgZ1A?
zu+skG`hOJIe-yy@uYF@h|6=-oGy&_skzj8S2he}p#ESmK_5Yy%M}px0QDRpE5Q$dx
zPmAUMHSGVD$szt960EepNd6xw_<ykf2MI9#Yu{MWzexTcDfoXd{zC%8GBT}3uF*3J
zos72x22!b2Dh-rg#WE_IBxNd!C1py`f7`^0{zdZtNWuSu{vQc~|3`~m4L~GX)juP$
z{|Ebjkzl3$#r6MS{|^#i{MWv*qJMGyKiL0^1dT?cBN&}du2d3omBK&}dX-8~X;~dh
z^39}^8Cb1cF9ZFzO|0l&T>lUHe<TS0A0u`(0Fh|YzcEqIS`Xv*>}A-{KiMq)3+Dhr
z|Br+S^|)|Mh@B^SH9EAIzt;GQVV)hZbnjyrwj>wB+;0jA3v4kSmb<}ZdXndaagmW2
z9|JFx#0lx}uC-rOmgQ9a(y!A&b5QUmCerhO2!ANhB2D^_H^sT+@OSK4;(w78%>SU_
z>Gp331mpZN9B6+b{s%?TG_*eoT4;YE5Rfy=a9IB|&CmZq`@5c2+h6blol}PO@gJ$6
znxFp<&;QUs*a>d$L6LYk9)m|2Q@Ae=K163qNN9LFf=$yTnB&<&+?8QCNh(w_N-3w6
zcnF`lmvA5cvN6$^V${SB2*cIFJ6!mU)b7&lJ<q_QYO0=8Q)+ka_HL_jq#6v-SPi>#
zw|8~dvsJ_H-0gkH;7~Ql#%kD|yS;Or&sGh)b9YIjP>t1aRR2%ef7b+oblm=v3Ifi5
zKmyo*u>WBH!TvXd{ATr`ya(V48)NnP4^3Idf9J#w_8$fPg!tclv*W48lo?^TnPuZy
zJq{0i+XcM-|2K2*qB3mhpORVVpML@j`bWX<jfpHyNE=>A;RQ~F1poj50000000000
S0001J1N;{VbU3j97y$r>Aaw))

literal 0
Hc$@<O00001

diff --git a/t/t4000-upgrade/make-repo.sh b/t/t4000-upgrade/make-repo.sh
new file mode 100644
index 0000000..98b5020
--- /dev/null
+++ b/t/t4000-upgrade/make-repo.sh
@@ -0,0 +1,71 @@
+# This script makes several versions of a small test repository that
+# can be used for testing the format version upgrade code.
+
+LANG=C
+LC_ALL=C
+PAGER=cat
+TZ=UTC
+export LANG LC_ALL PAGER TZ
+unset AUTHOR_DATE
+unset AUTHOR_EMAIL
+unset AUTHOR_NAME
+unset COMMIT_AUTHOR_EMAIL
+unset COMMIT_AUTHOR_NAME
+unset GIT_ALTERNATE_OBJECT_DIRECTORIES
+unset GIT_AUTHOR_DATE
+GIT_AUTHOR_EMAIL=author@example.com
+GIT_AUTHOR_NAME='A U Thor'
+unset GIT_COMMITTER_DATE
+GIT_COMMITTER_EMAIL=committer@example.com
+GIT_COMMITTER_NAME='C O Mitter'
+unset GIT_DIFF_OPTS
+unset GIT_DIR
+unset GIT_EXTERNAL_DIFF
+unset GIT_INDEX_FILE
+unset GIT_OBJECT_DIRECTORY
+unset SHA1_FILE_DIRECTORIES
+unset SHA1_FILE_DIRECTORY
+export GIT_AUTHOR_EMAIL GIT_AUTHOR_NAME
+export GIT_COMMITTER_EMAIL GIT_COMMITTER_NAME
+
+for ver in 0.12 0.8; do
+    if [ -e $ver.tar.gz ]; then continue; fi
+
+    # Get the required stgit version.
+    (
+        cd ../..
+        git archive --format=tar --prefix=stgit-$ver/ v$ver
+    ) | tar xf -
+
+    # Set up a repository.
+    mkdir $ver
+    cd $ver
+    git init
+    touch foo
+    git add foo
+    git commit -m 'Initial commit'
+
+    # Use the old stgit.
+    (
+        pwd
+        PATH=../stgit-$ver:$PATH
+
+        stg --version
+        stg init
+        echo 'cool branch' > .git/patches/master/description
+
+        for i in 0 1 2 3 4; do
+            stg new p$i -m "Patch $i"
+            echo "Line $i" >> foo
+            stg refresh
+        done
+        stg pop -n 2
+    )
+
+    # Reduce the number of small files.
+    git gc
+
+    # Make a tarball.
+    cd ..
+    tar zcf $ver.tar.gz $ver
+done
Karl Hasselström· May 19, 2007, 00:10 UTC · re: Karl Hasselström · lore

[StGIT PATCH 5/5] Add --binary flag to commands that generate diffs

This just passes the --binary option to git-diff-*, which causes the generated diffs to contain an applyable diff even when binary files differ. It's necessary to do this if you want to mail patches to binary files.

Signed-off-by: Karl Hasselström <kha@treskal.com>
---
 stgit/commands/diff.py   |    6 +++++-
 stgit/commands/export.py |    5 ++++-
 stgit/commands/mail.py   |    6 +++++-
 stgit/git.py             |   16 ++++++++++++----
 4 files changed, 26 insertions(+), 7 deletions(-)
Show changes to 4 files +26 −7

stgit/commands/diff.py, stgit/commands/export.py, stgit/commands/mail.py, stgit/git.py

diff --git a/stgit/commands/diff.py b/stgit/commands/diff.py
index 8678a0a..d3e1190 100644
--- a/stgit/commands/diff.py
+++ b/stgit/commands/diff.py
@@ -44,6 +44,9 @@ shows the specified patch (defaulting to the current one)."""
 options = [make_option('-r', '--range',
                        metavar = 'rev1[..[rev2]]', dest = 'revs',
                        help = 'show the diff between revisions'),
+           make_option('--binary',
+                       help = 'output a diff even for binary files',
+                       action = 'store_true'),
            make_option('-s', '--stat',
                        help = 'show the stat instead of the diff',
                        action = 'store_true')]
@@ -79,6 +82,7 @@ def func(parser, options, args):
     if options.stat:
         print git.diffstat(args, git_id(rev1), git_id(rev2))
     else:
-        diff_str = git.diff(args, git_id(rev1), git_id(rev2))
+        diff_str = git.diff(args, git_id(rev1), git_id(rev2),
+                            binary = options.binary)
         if diff_str:
             pager(diff_str)
diff --git a/stgit/commands/export.py b/stgit/commands/export.py
index 79b8630..20d8f67 100644
--- a/stgit/commands/export.py
+++ b/stgit/commands/export.py
@@ -62,6 +62,9 @@ options = [make_option('-d', '--dir',
                        help = 'Use FILE as a template'),
            make_option('-b', '--branch',
                        help = 'use BRANCH instead of the default one'),
+           make_option('--binary',
+                       help = 'output a diff even for binary files',
+                       action = 'store_true'),
            make_option('-s', '--stdout',
                        help = 'dump the patches to the standard output',
                        action = 'store_true')]
@@ -172,7 +175,7 @@ def func(parser, options, args):
         # write the diff
         git.diff(rev1 = patch.get_bottom(),
                  rev2 = patch.get_top(),
-                 out_fd = f)
+                 out_fd = f, binary = options.binary)
         if not options.stdout:
             f.close()
         patch_no += 1
diff --git a/stgit/commands/mail.py b/stgit/commands/mail.py
index 151a408..2fcaa5f 100644
--- a/stgit/commands/mail.py
+++ b/stgit/commands/mail.py
@@ -120,6 +120,9 @@ options = [make_option('-a', '--all',
                        help = 'username for SMTP authentication'),
            make_option('-b', '--branch',
                        help = 'use BRANCH instead of the default one'),
+           make_option('--binary',
+                       help = 'output a diff even for binary files',
+                       action = 'store_true'),
            make_option('-m', '--mbox',
                        help = 'generate an mbox file instead of sending',
                        action = 'store_true')]
@@ -390,7 +393,8 @@ def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
                  # for backward template compatibility
                  'endofheaders': '',
                  'diff':         git.diff(rev1 = git_id('%s//bottom' % patch),
-                                          rev2 = git_id('%s//top' % patch)),
+                                          rev2 = git_id('%s//top' % patch),
+                                          binary = options.binary),
                  'diffstat':     git.diffstat(rev1 = git_id('%s//bottom'%patch),
                                               rev2 = git_id('%s//top' % patch)),
                  # for backward template compatibility
diff --git a/stgit/git.py b/stgit/git.py
index 837f927..86630ce 100644
--- a/stgit/git.py
+++ b/stgit/git.py
@@ -771,20 +771,28 @@ def status(files = None, modified = False, new = False, deleted = False,
         else:
             print '%s' % fs[1]
 
-def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
+def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None,
+         binary = False):
     """Show the diff between rev1 and rev2
     """
     if not files:
         files = []
 
+    args = []
+    if binary:
+        args.append('--binary')
+
     if rev1 and rev2:
-        diff_str = _output(['git-diff-tree', '-p', rev1, rev2, '--'] + files)
+        diff_str = _output(['git-diff-tree', '-p'] + args
+                           + [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']
+                               + args + [rev2, '--'] + files)
         else:
-            diff_str = _output(['git-diff-index', '-p', rev1, '--'] + files)
+            diff_str = _output(['git-diff-index', '-p']
+                               + args + [rev1, '--'] + files)
     else:
         diff_str = ''
 
Catalin Marinas· May 22, 2007, 12:15 UTC · re: Karl Hasselström · lore

Re: [StGIT PATCH 5/5] Add --binary flag to commands that generate diffs

On 19/05/07, Karl Hasselström <kha@treskal.com> wrote:
> This just passes the --binary option to git-diff-*, which causes the
> generated diffs to contain an applyable diff even when binary files
> differ. It's necessary to do this if you want to mail patches to
> binary files.

I applied this patch but is there anything wrong if we have this option on by default, at least for some commands? Maybe we don't need it for 'show' and 'diff' but we definitely need it for 'mail' and 'export'.

There is also git.apply_diff() which calls git.diff(). This is first tried when pushing a patch and followed by a three-way merged if it fails. I think we should always have the --binary option in this case.

-- 
Catalin
Karl Hasselström· May 22, 2007, 13:31 UTC · re: Catalin Marinas · lore

Re: [StGIT PATCH 5/5] Add --binary flag to commands that generate diffs

On 2007-05-22 13:15:13 +0100, Catalin Marinas wrote:
Show 11 quoted lines
> On 19/05/07, Karl Hasselström <kha@treskal.com> wrote:
>
> > This just passes the --binary option to git-diff-*, which causes
> > the generated diffs to contain an applyable diff even when binary
> > files differ. It's necessary to do this if you want to mail
> > patches to binary files.
>
> I applied this patch but is there anything wrong if we have this
> option on by default, at least for some commands? Maybe we don't
> need it for 'show' and 'diff' but we definitely need it for 'mail'
> and 'export'.
I'd be fine with that.
> There is also git.apply_diff() which calls git.diff(). This is first
> tried when pushing a patch and followed by a three-way merged if it
> fails. I think we should always have the --binary option in this
> case.
Yes, that sounds good.
-- 
Karl Hasselström, kha@treskal.com
      www.treskal.com/kalle

← back to recent threads