Merge lp:~eric97/bzr/regenerate-pack-names into lp:bzr

Proposed by Eric Siegerman
Status: Work in progress
Proposed branch: lp:~eric97/bzr/regenerate-pack-names
Merge into: lp:bzr
Diff against target: 455 lines (+325/-8)
8 files modified
bzrlib/builtins.py (+32/-0)
bzrlib/repofmt/pack_repo.py (+53/-4)
bzrlib/repository.py (+12/-1)
bzrlib/tests/__init__.py (+1/-1)
bzrlib/tests/blackbox/__init__.py (+1/-0)
bzrlib/tests/blackbox/test_regenerate_pack_names.py (+131/-0)
bzrlib/tests/per_pack_repository.py (+91/-2)
doc/en/release-notes/bzr-2.4.txt (+4/-0)
To merge this branch: bzr merge lp:~eric97/bzr/regenerate-pack-names
Reviewer Review Type Date Requested Status
Martin Pool Needs Fixing
Review via email: mp+49756@code.launchpad.net

Description of the change

I clobbered the pack-names file in one of my shared repos. (Not
to worry; I'm 100% certain it was my screwup, *not* Bazaar's.)

This MP is the result. It adds a hidden "regenerate-pack-names"
command.

It does *not* actually overwrite .bzr/repository/pack-names, but
rather writes its new version to "pack-names.generated", for the
user to inspect and, if it looks good, rename to the live
filename. (Yeah, I know JAM's new repair-dirstate command does
the repairs in-place, but pack-names seems a far more dangerous
file to replace, so I wanted to build in an extra layer of
paranoia.)

Two issues before it's ready to merge:
  - Locking: Does this need to lock the repo? If so, read or
    write? Strictly, it should block other processes from even
    renaming new packs into packs/*.pack for the duration, but is
    that even possible? Not much point locking the
    rewrite-pack-names mutex, because it doesn't in fact write
    that file...

  - So far, I've only tested against local file access; no
    remote Transports. I wonder whether there's any point
    supporting it remotely, given that you need local shell
    access to rename the new file into service anyway.

To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote :

Thanks for addressing that gap. I don't recall hearing of anyone else hitting that exact problem, but it's certainly good to have ways for people to recover whenever we can.

For consistency I think we should call this repair-pack-names.

I think you should hold a lock while doing this. I think the repo lock is held while this is changed. You should check.

--force doesn't seem to have any effect, and the help implies it does.

I see your point about writing it to a temporary file, but I think that just makes it more difficult for people who've got into this state. I would rather for example back up the existing file, or indeed back up the whole repo dir, or show a description of the differences. For users who are not super technical and perhaps have their tree stored on a remote file server, asking them to also find and rename the file just makes it more difficult.

regenerate_pack_names seems likely to be duplicating some knowledge about the file format that's already present.

+ (hdr, err) = self.run_bzr_error([], ['dump-btree', '--raw', path],
+ retcode=0)

It would be cleaner and faster to use an API call for these rather than starting a new copy of bzr.

+ def assertFileExists(self, path):
+ self.assertTrue(os.path.exists(path), "%s exists" % path)
+
+ def assertNotFileExists(self, path):
+ self.assertFalse(os.path.exists(path), "%s does not exist" % path)
+

There is already failIfExists and failUnlessExists in TestCase.

  - So far, I've only tested against local file access; no
    remote Transports. I wonder whether there's any point
    supporting it remotely, given that you need local shell
    access to rename the new file into service anyway.

You're not doing anything transport-specific so I don't see any reason you would need to run per-transport tests. However, it would be good to make the unit tests run against a memory transport because that will be faster and it will validate you're not doing anything accidentally local fs specific.

If you want help with anything just ask.

Thanks, Martin

review: Needs Fixing
Revision history for this message
Eric Siegerman (eric97) wrote :

I've set this back to Work in Progress while I address Martin's comments.

Revision history for this message
Martin Pool (mbp) wrote :

On 19 February 2011 06:08, Eric Siegerman <email address hidden> wrote:
> I've set this back to Work in Progress while I address Martin's comments.

If you want help, or want someone else to finish it, just ask.

Unmerged revisions

5671. By Eric Siegerman <email address hidden>

Blackbox tests now verify that the old pack-names wasn't touched.

5670. By Eric Siegerman <email address hidden>

Doc and release-note fixes.

5669. By Eric Siegerman <email address hidden>

Add a test case, and rename another one.

5668. By Eric Siegerman <email address hidden>

Implement pack_repo.RepositoryPackCollection.regenerate_pack_names().

5667. By Eric Siegerman <email address hidden>

Push the implementation down a couple more levels.
It still doesn't do anything, but checks for a bunch of error
conditions first.

5666. By Eric Siegerman <email address hidden>

A couple more doc fixes: simple typos.

5665. By Eric Siegerman <email address hidden>

Drive-by doc fix: TestCase.run_captured() doesn't exist, but .run_bzr_error() captures output,
which is why the comment in question seems to think one might want to "see also" it.

5664. By Eric Siegerman <email address hidden>

Drive-by doc fix.

5663. By Eric Siegerman <email address hidden>

Skeleton.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/builtins.py'
--- bzrlib/builtins.py 2011-02-09 17:10:05 +0000
+++ bzrlib/builtins.py 2011-02-15 02:33:59 +0000
@@ -537,6 +537,38 @@
537 raise errors.BzrCommandError('failed to reset the tree state'537 raise errors.BzrCommandError('failed to reset the tree state'
538 + extra)538 + extra)
539539
540class cmd_regenerate_pack_names(Command):
541 __doc__ = """Regenerate the pack-names file.
542
543 This is not meant to be used normally, but only as a way to recover from
544 filesystem corruption, etc. This creates a new .bzr/repository/pack-names
545 file for the repository from scratch, including in it all packs in the
546 repository's packs directory.
547
548 The existing pack-names file is *not* overwritten; the new file is written
549 to .bzr/repository/pack-names.generated. After running this command, you
550 should look over the new file, e.g. using `bzr dump-btree`; then, if it
551 looks OK, move it into place manually.
552 """
553
554 takes_options = ['directory',
555 Option('force',
556 help='Regenerate the file even if it doesn\'t appear to be'
557 ' corrupted.'),
558 ]
559 hidden = True
560
561 def run(self, directory='.', force=False):
562 bzr_dir = bzrdir.BzrDir.open(directory)
563 repository = bzr_dir.open_repository()
564 try:
565 new_content = repository.regenerate_pack_names()
566 except NotImplementedError:
567 raise errors.BzrCommandError(directory + ': not a pack repository')
568 t = repository._transport
569 mode=repository.bzrdir._get_file_mode()
570 t.put_file('pack-names.generated', new_content, mode)
571
540572
541class cmd_revno(Command):573class cmd_revno(Command):
542 __doc__ = """Show current revision number.574 __doc__ = """Show current revision number.
543575
=== modified file 'bzrlib/repofmt/pack_repo.py'
--- bzrlib/repofmt/pack_repo.py 2011-01-20 21:15:10 +0000
+++ bzrlib/repofmt/pack_repo.py 2011-02-15 02:33:59 +0000
@@ -1896,10 +1896,11 @@
1896 def _diff_pack_names(self):1896 def _diff_pack_names(self):
1897 """Read the pack names from disk, and compare it to the one in memory.1897 """Read the pack names from disk, and compare it to the one in memory.
18981898
1899 :return: (disk_nodes, deleted_nodes, new_nodes)1899 :return: (disk_nodes, deleted_nodes, new_nodes, orig_disk_nodes)
1900 disk_nodes The final set of nodes that should be referenced1900 disk_nodes The final set of nodes that should be referenced
1901 deleted_nodes Nodes which have been removed from when we started1901 deleted_nodes Nodes which have been removed from when we started
1902 new_nodes Nodes that are newly introduced1902 new_nodes Nodes that are newly introduced
1903 orig_disk_nodes Original set of nodes that are already referenced
1903 """1904 """
1904 # load the disk nodes across1905 # load the disk nodes across
1905 disk_nodes = set()1906 disk_nodes = set()
@@ -2216,6 +2217,47 @@
2216 for token in tokens:2217 for token in tokens:
2217 self._resume_pack(token)2218 self._resume_pack(token)
22182219
2220 def regenerate_pack_names(self):
2221 """Build a new pack-names file by examining the pack and index files on disk.
2222
2223 Does *not* modify the existing pack-names file, but returns the contents
2224 for a new one.
2225
2226 :return: The contents that (this method believes) should be written
2227 to pack-names
2228 """
2229 if not self._pack_transport.listable():
2230 raise Exception("can't list repository's contents")
2231
2232 # Paranoia: make sure we don't have *anything* cached
2233 self.reset()
2234
2235 builder = self._index_builder_class()
2236 files = self._pack_transport.list_dir('.')
2237 for file in files:
2238 if not file.endswith('.pack'):
2239 continue
2240 pack_name = file[0:-5]
2241
2242 sizes = [None, None, None, None]
2243 if self.chk_index is not None:
2244 sizes.append(None)
2245 for suffix in self._suffix_offsets.keys():
2246 if suffix == '.cix' and self.chk_index is None:
2247 continue
2248 size_offset = self._suffix_offsets[suffix]
2249 index_name = pack_name + suffix
2250 size = self._index_transport.stat(index_name).st_size
2251 sizes[size_offset] = str(size)
2252
2253 builder.add_node((pack_name,), " ".join(sizes))
2254
2255 # More paranoia: if anything did make it back into the cache,
2256 # our caller might well be about to change things out from under it
2257 self.reset()
2258
2259 return builder.finish()
2260
22192261
2220class KnitPackRepository(KnitRepository):2262class KnitPackRepository(KnitRepository):
2221 """Repository with knit objects stored inside pack containers.2263 """Repository with knit objects stored inside pack containers.
@@ -2432,6 +2474,13 @@
2432 packer = ReconcilePacker(collection, packs, extension, revs)2474 packer = ReconcilePacker(collection, packs, extension, revs)
2433 return packer.pack(pb)2475 return packer.pack(pb)
24342476
2477 def regenerate_pack_names(self):
2478 """Regenerate the pack-names file within the repository.
2479
2480 This is only for disaster recovery; it should not be called in day-to-day use.
2481 """
2482 return self._pack_collection.regenerate_pack_names()
2483
2435 @only_raises(errors.LockNotHeld, errors.LockBroken)2484 @only_raises(errors.LockNotHeld, errors.LockBroken)
2436 def unlock(self):2485 def unlock(self):
2437 if self._write_lock_count == 1 and self._write_group is not None:2486 if self._write_lock_count == 1 and self._write_group is not None:
24382487
=== modified file 'bzrlib/repository.py'
--- bzrlib/repository.py 2011-02-08 15:41:44 +0000
+++ bzrlib/repository.py 2011-02-15 02:33:59 +0000
@@ -1439,7 +1439,7 @@
1439 def lock_write(self, token=None):1439 def lock_write(self, token=None):
1440 """Lock this repository for writing.1440 """Lock this repository for writing.
14411441
1442 This causes caching within the repository obejct to start accumlating1442 This causes caching within the repository object to start accumlating
1443 data during reads, and allows a 'write_group' to be obtained. Write1443 data during reads, and allows a 'write_group' to be obtained. Write
1444 groups must be used for actual data insertion.1444 groups must be used for actual data insertion.
14451445
@@ -2866,6 +2866,17 @@
2866 """2866 """
2867 raise NotImplementedError(self.revision_graph_can_have_wrong_parents)2867 raise NotImplementedError(self.revision_graph_can_have_wrong_parents)
28682868
2869 def regenerate_pack_names(self):
2870 """Build a new pack-names file by examining the pack and index files on disk.
2871
2872 Does *not* modify the existing pack-names file, but returns the contents
2873 for a new one.
2874
2875 :return: The contents that (this method believes) should be written
2876 to pack-names
2877 """
2878 raise NotImplementedError(self.regenerate_pack_names)
2879
28692880
2870def install_revision(repository, rev, revision_tree):2881def install_revision(repository, rev, revision_tree):
2871 """Install all revision data into a repository."""2882 """Install all revision data into a repository."""
28722883
=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2011-02-11 17:12:35 +0000
+++ bzrlib/tests/__init__.py 2011-02-15 02:33:59 +0000
@@ -1896,7 +1896,7 @@
1896 or a functional test of the library.)1896 or a functional test of the library.)
18971897
1898 This sends the stdout/stderr results into the test's log,1898 This sends the stdout/stderr results into the test's log,
1899 where it may be useful for debugging. See also run_captured.1899 where it may be useful for debugging. See also run_bzr_error.
19001900
1901 :keyword stdin: A string to be used as stdin for the command.1901 :keyword stdin: A string to be used as stdin for the command.
1902 :keyword retcode: The status code the command should return;1902 :keyword retcode: The status code the command should return;
19031903
=== modified file 'bzrlib/tests/blackbox/__init__.py'
--- bzrlib/tests/blackbox/__init__.py 2011-01-27 17:45:24 +0000
+++ bzrlib/tests/blackbox/__init__.py 2011-02-15 02:33:59 +0000
@@ -99,6 +99,7 @@
99 'test_remove',99 'test_remove',
100 'test_re_sign',100 'test_re_sign',
101 'test_remove_tree',101 'test_remove_tree',
102 'test_regenerate_pack_names',
102 'test_repair_workingtree',103 'test_repair_workingtree',
103 'test_resolve',104 'test_resolve',
104 'test_revert',105 'test_revert',
105106
=== added file 'bzrlib/tests/blackbox/test_regenerate_pack_names.py'
--- bzrlib/tests/blackbox/test_regenerate_pack_names.py 1970-01-01 00:00:00 +0000
+++ bzrlib/tests/blackbox/test_regenerate_pack_names.py 2011-02-15 02:33:59 +0000
@@ -0,0 +1,131 @@
1# Copyright (C) 2011 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17
18import os
19from bzrlib import (
20 workingtree,
21 )
22from bzrlib.tests import TestCaseWithTransport
23
24
25class TestRegeneratePackNames(TestCaseWithTransport):
26
27 def get_btree_content(self, path):
28 """Produce a text dump of a B-Tree file.
29
30 :return: A hybrid dump: a --raw dump of the header,
31 followed by a formatted dump of the content
32 """
33 (hdr, err) = self.run_bzr_error([], ['dump-btree', '--raw', path],
34 retcode=0)
35 self.assertEquals("", err)
36 data_offset = hdr.find("\nPage 0")
37 if data_offset != -1:
38 hdr = hdr[0:data_offset]
39
40 (out, err) = self.run_bzr_error([], ['dump-btree', path], retcode=0)
41 self.assertEquals("", err)
42 return hdr + out
43
44 def assertFileExists(self, path):
45 self.assertTrue(os.path.exists(path), "%s exists" % path)
46
47 def assertNotFileExists(self, path):
48 self.assertFalse(os.path.exists(path), "%s does not exist" % path)
49
50 def assertFileContains(self, path, expected):
51 """Check an actual pack-names file"""
52 if expected is None:
53 self.assertNotFileExists(path)
54 else:
55 self.assertFileExists(path)
56 actual = self.get_btree_content(path)
57 self.assertEqualDiff(expected, actual)
58
59 def check_files(self, live_expected, gen_expected):
60 """Check the pack-names files after a test run.
61
62 :param live_expected: Expected content of the real pack-names file
63 (which will be its original contents unless our caller explicitly
64 modified the file). If None, the file is expected *not* to exist.
65 :param gen_expected: Expected content of the pack-names.generated
66 file that might have been produced from the code under test.
67 If None, the file is expected *not* to exist.
68 """
69
70 self.assertFileContains("tree/.bzr/repository/pack-names",
71 live_expected)
72 self.assertFileContains("tree/.bzr/repository/pack-names.generated",
73 gen_expected)
74
75 def make_initial_tree(self, format=None, read_pack_names=True):
76 tree = self.make_branch_and_tree('tree', format=format)
77 self.build_tree(['tree/foo', 'tree/dir/', 'tree/dir/bar'])
78 tree.add(['foo', 'dir', 'dir/bar'])
79 tree.commit('first')
80 if read_pack_names:
81 content = self.get_btree_content("tree/.bzr/repository/pack-names")
82 else:
83 content = None
84 return tree, content
85
86 # Success cases
87
88 def test_regenerate_pack_names_2a(self):
89 tree, old_pack_names = self.make_initial_tree(format="2a",
90 read_pack_names=True)
91 self.run_bzr('regenerate-pack-names -d tree')
92 self.check_files(old_pack_names, old_pack_names)
93
94 def test_regenerate_pack_names_default(self):
95 """Some day a new format might become the default. Then,
96 this test will have its moment in the sun :-)
97 """
98 tree, old_pack_names = self.make_initial_tree()
99 self.run_bzr('regenerate-pack-names -d tree')
100 self.check_files(old_pack_names, old_pack_names)
101
102 # Error cases
103
104 def test_regenerate_pack_names_knit(self):
105 """Can't regenerate-pack-names in a repo that doesn't have one"""
106 tree, old_pack_names = self.make_initial_tree(format="knit",
107 read_pack_names=False)
108 self.run_bzr_error([": not a pack repository"],
109 'regenerate-pack-names -d tree')
110 self.check_files(None, None)
111
112 def test_regenerate_pack_names_dir_no_bzrdir(self):
113 """regenerate-pack-names should fail on a directory that has no .bzr"""
114 tree, old_pack_names = self.make_initial_tree()
115 self.run_bzr_error(['Not a branch'],
116 'regenerate-pack-names -d tree/dir')
117 self.check_files(old_pack_names, None)
118
119 def test_regenerate_pack_names_file(self):
120 """regenerate-pack-names should fail on a file"""
121 tree, old_pack_names = self.make_initial_tree()
122 self.run_bzr_error(['Not a branch'],
123 'regenerate-pack-names -d tree/foo')
124 self.check_files(old_pack_names, None)
125
126 def test_regenerate_pack_names_nonexistent(self):
127 """regenerate-pack-names should fail on a nonexistent pathname"""
128 tree, old_pack_names = self.make_initial_tree()
129 self.run_bzr_error(['Not a branch'],
130 'regenerate-pack-names -d nonex')
131 self.check_files(old_pack_names, None)
0132
=== modified file 'bzrlib/tests/per_pack_repository.py'
--- bzrlib/tests/per_pack_repository.py 2011-01-26 19:34:58 +0000
+++ bzrlib/tests/per_pack_repository.py 2011-02-15 02:33:59 +0000
@@ -19,6 +19,10 @@
19These tests are repeated for all pack-based repository formats.19These tests are repeated for all pack-based repository formats.
20"""20"""
2121
22from os import (
23 listdir,
24 remove,
25 )
22from stat import S_ISDIR26from stat import S_ISDIR
2327
24from bzrlib.btree_index import BTreeGraphIndex28from bzrlib.btree_index import BTreeGraphIndex
@@ -53,7 +57,7 @@
5357
54 The following are populated from the test scenario:58 The following are populated from the test scenario:
5559
56 :ivar format_name: Registered name fo the format to test.60 :ivar format_name: Registered name of the format to test.
57 :ivar format_string: On-disk format marker.61 :ivar format_string: On-disk format marker.
58 :ivar format_supports_external_lookups: Boolean.62 :ivar format_supports_external_lookups: Boolean.
59 """63 """
@@ -710,7 +714,7 @@
710714
711 def _lock_write(self, write_lockable):715 def _lock_write(self, write_lockable):
712 """Lock write_lockable, add a cleanup and return the result.716 """Lock write_lockable, add a cleanup and return the result.
713 717
714 :param write_lockable: An object with a lock_write method.718 :param write_lockable: An object with a lock_write method.
715 :return: The result of write_lockable.lock_write().719 :return: The result of write_lockable.lock_write().
716 """720 """
@@ -1120,6 +1124,91 @@
1120 self.assertEqual(2, streaming_calls)1124 self.assertEqual(2, streaming_calls)
11211125
11221126
1127class TestRegeneratePackNames(TestCaseWithTransport):
1128 def get_format(self):
1129 return bzrdir.format_registry.make_bzrdir(self.format_name)
1130
1131 def read_file(self, path):
1132 f = file(path, 'rb')
1133 try:
1134 return f.read()
1135 finally:
1136 f.close()
1137
1138 self.fail("Should never get here")
1139
1140 def make_initial_tree(self, num_commits):
1141 """Build and populate a working tree.
1142
1143 :param num_commits: Number of commits to perform (for sufficiently small
1144 values, i.e. until autopacking happens, this is also the number of
1145 packs to create)
1146 :returns: (tree, old_pack_names)
1147 tree The WorkingTree
1148 old_pack_names Content of the repository's pack-names file
1149 """
1150 format = self.get_format()
1151 tree = self.make_branch_and_tree('tree', format=format)
1152 self.build_tree(['tree/foo', 'tree/dir/', 'tree/dir/bar'])
1153 tree.add(['foo', 'dir', 'dir/bar'])
1154 for i in range(1, num_commits + 1):
1155 tree.commit("revision #%d" % i)
1156 old_pack_names = self.read_file('tree/.bzr/repository/pack-names')
1157 return tree, old_pack_names
1158
1159 def run_and_check(self, tree, live_expected, gen_expected):
1160 """Run the method under test, and check the results.
1161
1162 :param tree: The working tree
1163 :param live_expected: Expected content of the real pack-names file
1164 (which will be its original contents unless our caller explicitly
1165 modified the file)
1166 :param gen_expected: Expected content of the newly generated pack-names
1167 """
1168 # Run the code under test
1169 reader = tree.branch.repository.regenerate_pack_names()
1170 # Check its output...
1171 gen_actual = reader.read()
1172 self.assertEqualDiff(gen_expected, gen_actual)
1173 # ...and the live pack-names file
1174 live_actual = self.read_file('tree/.bzr/repository/pack-names')
1175 self.assertEqualDiff(live_expected, live_actual)
1176
1177 def test_zero_packs(self):
1178 tree, old_pack_names = self.make_initial_tree(0)
1179 self.run_and_check(tree, old_pack_names, old_pack_names)
1180
1181 def test_one_pack(self):
1182 tree, old_pack_names = self.make_initial_tree(1)
1183 self.run_and_check(tree, old_pack_names, old_pack_names)
1184
1185 def test_three_packs(self):
1186 tree, old_pack_names = self.make_initial_tree(3)
1187 self.run_and_check(tree, old_pack_names, old_pack_names)
1188
1189 def test_with_real_pack_names_clobbered(self):
1190 """With the real pack-names clobbered, should succeed normally"""
1191 tree, old_pack_names = self.make_initial_tree(1)
1192 f = file('tree/.bzr/repository/pack-names', 'w')
1193 f.write("bogus\n")
1194 f.close()
1195 self.run_and_check(tree, "bogus\n", old_pack_names)
1196
1197 def test_with_an_index_deleted(self):
1198 """Hard-core repo corruption should raise an appropriate exception"""
1199 tree, old_pack_names = self.make_initial_tree(1)
1200 indices = listdir('tree/.bzr/repository/indices')
1201 for index in indices:
1202 if index.endswith('.tix'): # .tix chosen one at random
1203 remove('tree/.bzr/repository/indices/' + index)
1204
1205 self.assertRaises(errors.NoSuchFile,
1206 tree.branch.repository.regenerate_pack_names)
1207
1208 new_pack_names = self.read_file('tree/.bzr/repository/pack-names')
1209 self.assertEqualDiff(old_pack_names, new_pack_names)
1210
1211
1123def load_tests(basic_tests, module, loader):1212def load_tests(basic_tests, module, loader):
1124 # these give the bzrdir canned format name, and the repository on-disk1213 # these give the bzrdir canned format name, and the repository on-disk
1125 # format string1214 # format string
11261215
=== modified file 'doc/en/release-notes/bzr-2.4.txt'
--- doc/en/release-notes/bzr-2.4.txt 2011-02-14 12:03:05 +0000
+++ doc/en/release-notes/bzr-2.4.txt 2011-02-15 02:33:59 +0000
@@ -36,6 +36,10 @@
36 the dirstate file to be rebuilt, rather than using a ``bzr checkout``36 the dirstate file to be rebuilt, rather than using a ``bzr checkout``
37 workaround. (John Arbash Meinel)37 workaround. (John Arbash Meinel)
3838
39* A new hidden command ``bzr regenerate-pack-names``. This builds a new
40 pack-names file (useful if the old one was lost or damaged).
41 (Eric Siegerman)
42
39* Branching, merging and pulling a branch now copies revisions named in43* Branching, merging and pulling a branch now copies revisions named in
40 tags, not just the tag metadata. (Andrew Bennetts, #309682)44 tags, not just the tag metadata. (Andrew Bennetts, #309682)
41 45