~abentley/bzrtools/bzrtools.dev

0.1.1 by Michael Ellerman
Initial import
1
import os
2
import sys
0.1.87 by Michael Ellerman
Add support for detecting and upgrading from old format shelves.
3
from datetime import datetime
776.2.1 by Jelmer Vernooij
Remove unused imports and fix two imports.
4
from errors import CommandError, PatchFailed
0.1.56 by Michael Ellerman
Make HunkSelector agnostic as to whether it's selecting for shelving or
5
from hunk_selector import ShelveHunkSelector, UnshelveHunkSelector
610 by Aaron Bentley
Unify patch invocation
6
from patch import run_patch
776.2.1 by Jelmer Vernooij
Remove unused imports and fix two imports.
7
from patchsource import FilePatchSource
0.6.1 by Michael Ellerman
Win32 fixes from Alexander, slightly reworked by me.
8
from bzrlib.osutils import rename
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
9
10
class Shelf(object):
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
11
    MESSAGE_PREFIX = "# Shelved patch: "
12
0.2.22 by Michael Ellerman
Infrastructure to allow for multiple interchangeable shelves.
13
    _paths = {
14
        'base'          : '.shelf',
15
        'shelves'       : '.shelf/shelves',
16
        'current-shelf' : '.shelf/current-shelf',
17
    }
18
19
    def __init__(self, base, name=None):
20
        self.base = base
21
        self.__setup()
22
23
        if name is None:
24
            current = os.path.join(self.base, self._paths['current-shelf'])
25
            name = open(current).read().strip()
26
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
27
        assert '\n' not in name
0.2.5 by Michael Ellerman
Make some of the messages a bit more readable.
28
        self.name = name
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
29
0.2.22 by Michael Ellerman
Infrastructure to allow for multiple interchangeable shelves.
30
        self.dir = os.path.join(self.base, self._paths['shelves'], name)
31
        if not os.path.isdir(self.dir):
32
            os.mkdir(self.dir)
0.1.43 by Michael Ellerman
New shelf layout. Shelves now sit under .bzr/x-shelf/default/
33
0.2.22 by Michael Ellerman
Infrastructure to allow for multiple interchangeable shelves.
34
    def __setup(self):
35
        # Create required directories etc.
36
        for dir in [self._paths['base'], self._paths['shelves']]:
37
            dir = os.path.join(self.base, dir)
0.2.2 by Michael Ellerman
For the moment at least storing scads of stuff under .bzr isn't really
38
            if not os.path.isdir(dir):
39
                os.mkdir(dir)
0.1.43 by Michael Ellerman
New shelf layout. Shelves now sit under .bzr/x-shelf/default/
40
0.2.22 by Michael Ellerman
Infrastructure to allow for multiple interchangeable shelves.
41
        current = os.path.join(self.base, self._paths['current-shelf'])
42
        if not os.path.exists(current):
43
            f = open(current, 'w')
44
            f.write('default')
45
            f.close()
46
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
47
    def make_default(self):
48
        f = open(os.path.join(self.base, self._paths['current-shelf']), 'w')
49
        f.write(self.name)
50
        f.close()
51
        self.log("Default shelf is now '%s'\n" % self.name)
52
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
53
    def log(self, msg):
0.2.20 by Michael Ellerman
Make list output look nicer when the shelf is empty.
54
        sys.stderr.write(msg)
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
55
0.2.19 by Michael Ellerman
Add shelf delete subcommand, make list barf if it gets an arg.
56
    def delete(self, patch):
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
57
        path = self.__path_from_user(patch)
0.6.1 by Michael Ellerman
Win32 fixes from Alexander, slightly reworked by me.
58
        rename(path, '%s~' % path)
0.2.19 by Michael Ellerman
Add shelf delete subcommand, make list barf if it gets an arg.
59
0.1.110 by Michael Ellerman
Make the patch argument to 'shelf show' optional.
60
    def display(self, patch=None):
61
        if patch is None:
62
            path = self.last_patch()
598 by Aaron Bentley
Clean error when 'shelf show' invoked with no patches on shelf.
63
            if path is None:
64
                raise CommandError("No patches on shelf.")
0.1.110 by Michael Ellerman
Make the patch argument to 'shelf show' optional.
65
        else:
66
            path = self.__path_from_user(patch)
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
67
        sys.stdout.write(open(path).read())
68
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
69
    def list(self):
0.1.83 by Michael Ellerman
Cope if there's bogus files in the shelf directory.
70
        indexes = self.__list()
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
71
        self.log("Patches on shelf '%s':" % self.name)
0.2.20 by Michael Ellerman
Make list output look nicer when the shelf is empty.
72
        if len(indexes) == 0:
73
            self.log(' None\n')
74
            return
75
        self.log('\n')
76
        for index in indexes:
0.2.18 by Michael Ellerman
Make next_patch() cope with holes, eg. 00, 02 etc.
77
            msg = self.get_patch_message(self.__path(index))
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
78
            if msg is None:
79
                msg = "No message saved with patch."
0.2.20 by Michael Ellerman
Make list output look nicer when the shelf is empty.
80
            self.log(' %.2d: %s\n' % (index, msg))
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
81
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
82
    def __path_from_user(self, patch_id):
83
        try:
0.2.29 by Michael Ellerman
Better error message when trying to show an non-existant patch
84
            patch_index = int(patch_id)
0.3.2 by Michael Ellerman
Fix error handling for non-int patch arguments
85
        except (TypeError, ValueError):
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
86
            raise CommandError("Invalid patch name '%s'" % patch_id)
87
0.2.29 by Michael Ellerman
Better error message when trying to show an non-existant patch
88
        path = self.__path(patch_index)
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
89
90
        if not os.path.exists(path):
0.2.29 by Michael Ellerman
Better error message when trying to show an non-existant patch
91
            raise CommandError("Patch '%s' doesn't exist on shelf %s!" % \
92
                        (patch_id, self.name))
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
93
94
        return path
95
0.2.18 by Michael Ellerman
Make next_patch() cope with holes, eg. 00, 02 etc.
96
    def __path(self, index):
0.2.22 by Michael Ellerman
Infrastructure to allow for multiple interchangeable shelves.
97
        return os.path.join(self.dir, '%.2d' % index)
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
98
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
99
    def next_patch(self):
0.2.18 by Michael Ellerman
Make next_patch() cope with holes, eg. 00, 02 etc.
100
        indexes = self.__list()
101
102
        if len(indexes) == 0:
103
            next = 0
104
        else:
105
            next = indexes[-1] + 1
106
        return self.__path(next)
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
107
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
108
    def __list(self):
0.2.22 by Michael Ellerman
Infrastructure to allow for multiple interchangeable shelves.
109
        patches = os.listdir(self.dir)
0.1.83 by Michael Ellerman
Cope if there's bogus files in the shelf directory.
110
        indexes = []
111
        for f in patches:
112
            if f.endswith('~'):
113
                continue # ignore backup files
114
            try:
115
                indexes.append(int(f))
116
            except ValueError:
117
                self.log("Warning: Ignoring junk file '%s' on shelf.\n" % f)
118
0.2.18 by Michael Ellerman
Make next_patch() cope with holes, eg. 00, 02 etc.
119
        indexes.sort()
120
        return indexes
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
121
122
    def last_patch(self):
0.2.18 by Michael Ellerman
Make next_patch() cope with holes, eg. 00, 02 etc.
123
        indexes = self.__list()
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
124
0.2.18 by Michael Ellerman
Make next_patch() cope with holes, eg. 00, 02 etc.
125
        if len(indexes) == 0:
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
126
            return None
127
0.2.18 by Michael Ellerman
Make next_patch() cope with holes, eg. 00, 02 etc.
128
        return self.__path(indexes[-1])
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
129
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
130
    def get_patch_message(self, patch_path):
131
        patch = open(patch_path, 'r').read()
132
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
133
        if not patch.startswith(self.MESSAGE_PREFIX):
134
            return None
135
        return patch[len(self.MESSAGE_PREFIX):patch.index('\n')]
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
136
423.1.4 by Aaron Bentley
Add --no-color option to shelf
137
    def unshelve(self, patch_source, patch_name=None, all=False, force=False,
138
                 no_color=False):
0.1.87 by Michael Ellerman
Add support for detecting and upgrading from old format shelves.
139
        self._check_upgrade()
140
423.1.4 by Aaron Bentley
Add --no-color option to shelf
141
        if no_color is False:
142
            color = None
143
        else:
144
            color = False
0.1.113 by Michael Ellerman
Support for unshelving in arbitrary order.
145
        if patch_name is None:
146
            patch_path = self.last_patch()
147
        else:
148
            patch_path = self.__path_from_user(patch_name)
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
149
0.1.112 by Michael Ellerman
Code cleanup, more explicit variable names.
150
        if patch_path is None:
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
151
            raise CommandError("No patch found on shelf %s" % self.name)
152
0.1.112 by Michael Ellerman
Code cleanup, more explicit variable names.
153
        patches = FilePatchSource(patch_path).readpatches()
0.1.101 by Michael Ellerman
Cleanup naming. PatchSource gives us back Patches not Hunks.
154
        if all:
155
            to_unshelve = patches
0.1.58 by Michael Ellerman
Unshelve --pick was broken, because we deleted the whole patch, even when only
156
            to_remain = []
0.1.80 by Michael Ellerman
After extensive user testing, ie. me using it, I've decided --pick should be
157
        else:
423.1.4 by Aaron Bentley
Add --no-color option to shelf
158
            hs = UnshelveHunkSelector(patches, color)
159
            to_unshelve, to_remain = hs.select()
0.1.55 by Michael Ellerman
Add support for 'unshelve --pick'. This works but the UI is broken, as the
160
0.1.58 by Michael Ellerman
Unshelve --pick was broken, because we deleted the whole patch, even when only
161
        if len(to_unshelve) == 0:
0.2.3 by Michael Ellerman
Factor out bzrisms.
162
            raise CommandError('Nothing to unshelve')
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
163
0.1.112 by Michael Ellerman
Code cleanup, more explicit variable names.
164
        message = self.get_patch_message(patch_path)
0.2.14 by Michael Ellerman
Use new log function.
165
        if message is None:
0.1.78 by Michael Ellerman
I'm sure someone will complain about this, but remove the diffstat after
166
            message = "No message saved with patch."
167
        self.log('Unshelving from %s/%s: "%s"\n' % \
0.1.112 by Michael Ellerman
Code cleanup, more explicit variable names.
168
                (self.name, os.path.basename(patch_path), message))
0.1.58 by Michael Ellerman
Unshelve --pick was broken, because we deleted the whole patch, even when only
169
0.1.76 by Michael Ellerman
Add a test to make sure we don't delete the shelved patch if unshelving
170
        try:
0.1.77 by Michael Ellerman
When unshelving, try to patch with --dry-run first, if that fails bail out.
171
            self._run_patch(to_unshelve, dry_run=True)
0.1.76 by Michael Ellerman
Add a test to make sure we don't delete the shelved patch if unshelving
172
            self._run_patch(to_unshelve)
0.1.89 by Michael Ellerman
Add support for unshelving -p0 patches, for backward compatibility.
173
        except PatchFailed:
174
            try:
0.1.107 by Michael Ellerman
Assume -p0 format diffs by default, fall back to -p1 on failure.
175
                self._run_patch(to_unshelve, strip=1, dry_run=True)
176
                self._run_patch(to_unshelve, strip=1)
0.1.89 by Michael Ellerman
Add support for unshelving -p0 patches, for backward compatibility.
177
            except PatchFailed:
0.1.91 by Michael Ellerman
Add --force option to unshelve, which runs the shelved changes through
178
                if force:
179
                    self.log('Warning: Unshelving failed, forcing as ' \
180
                             'requested. Shelf will not be modified.\n')
181
                    try:
182
                        self._run_patch(to_unshelve)
183
                    except PatchFailed:
184
                        pass
185
                    return
0.1.89 by Michael Ellerman
Add support for unshelving -p0 patches, for backward compatibility.
186
                raise CommandError("Your shelved patch no " \
0.1.76 by Michael Ellerman
Add a test to make sure we don't delete the shelved patch if unshelving
187
                    "longer applies cleanly to the working tree!")
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
188
0.1.84 by Michael Ellerman
Backup the patch when we unshelve. Suggested by Christian Reis.
189
        # Backup the shelved patch
0.6.1 by Michael Ellerman
Win32 fixes from Alexander, slightly reworked by me.
190
        rename(patch_path, '%s~' % patch_path)
0.1.84 by Michael Ellerman
Backup the patch when we unshelve. Suggested by Christian Reis.
191
192
        if len(to_remain) > 0:
0.1.112 by Michael Ellerman
Code cleanup, more explicit variable names.
193
            f = open(patch_path, 'w')
0.1.101 by Michael Ellerman
Cleanup naming. PatchSource gives us back Patches not Hunks.
194
            for patch in to_remain:
195
                f.write(str(patch))
0.1.58 by Michael Ellerman
Unshelve --pick was broken, because we deleted the whole patch, even when only
196
            f.close()
0.1.35 by Michael Ellerman
Use DiffStat rather than calling out to /bin/diffstat
197
423.1.4 by Aaron Bentley
Add --no-color option to shelf
198
    def shelve(self, patch_source, all=False, message=None, no_color=False):
0.1.87 by Michael Ellerman
Add support for detecting and upgrading from old format shelves.
199
        self._check_upgrade()
423.1.4 by Aaron Bentley
Add --no-color option to shelf
200
        if no_color is False:
201
            color = None
202
        else:
203
            color = False
0.2.8 by Michael Ellerman
Always set shelf message and print it when shelving.
204
0.1.101 by Michael Ellerman
Cleanup naming. PatchSource gives us back Patches not Hunks.
205
        patches = patch_source.readpatches()
0.1.31 by Michael Ellerman
- Keep our branch around, and use it directly instead of bzr_root.
206
0.1.101 by Michael Ellerman
Cleanup naming. PatchSource gives us back Patches not Hunks.
207
        if all:
208
            to_shelve = patches
0.1.80 by Michael Ellerman
After extensive user testing, ie. me using it, I've decided --pick should be
209
        else:
423.1.4 by Aaron Bentley
Add --no-color option to shelf
210
            to_shelve = ShelveHunkSelector(patches, color).select()[0]
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
211
0.1.58 by Michael Ellerman
Unshelve --pick was broken, because we deleted the whole patch, even when only
212
        if len(to_shelve) == 0:
0.2.3 by Michael Ellerman
Factor out bzrisms.
213
            raise CommandError('Nothing to shelve')
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
214
0.2.8 by Michael Ellerman
Always set shelf message and print it when shelving.
215
        if message is None:
216
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
217
            message = "Changes shelved on %s" % timestamp
218
0.1.112 by Michael Ellerman
Code cleanup, more explicit variable names.
219
        patch_path = self.next_patch()
0.2.25 by Michael Ellerman
Fixup shelving message
220
        self.log('Shelving to %s/%s: "%s"\n' % \
0.1.112 by Michael Ellerman
Code cleanup, more explicit variable names.
221
                (self.name, os.path.basename(patch_path), message))
0.2.8 by Michael Ellerman
Always set shelf message and print it when shelving.
222
0.1.112 by Michael Ellerman
Code cleanup, more explicit variable names.
223
        f = open(patch_path, 'a')
0.2.8 by Michael Ellerman
Always set shelf message and print it when shelving.
224
225
        assert '\n' not in message
0.1.101 by Michael Ellerman
Cleanup naming. PatchSource gives us back Patches not Hunks.
226
        f.write("%s%s\n" % (self.MESSAGE_PREFIX, message))
227
228
        for patch in to_shelve:
229
            f.write(str(patch))
230
231
        f.flush()
232
        os.fsync(f.fileno())
233
        f.close()
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
234
0.1.94 by Michael Ellerman
Support for 0.7 format diffs when shelving, and better error handling
235
        try:
236
            self._run_patch(to_shelve, reverse=True, dry_run=True)
237
            self._run_patch(to_shelve, reverse=True)
238
        except PatchFailed:
239
            try:
0.1.107 by Michael Ellerman
Assume -p0 format diffs by default, fall back to -p1 on failure.
240
                self._run_patch(to_shelve, reverse=True, strip=1, dry_run=True)
241
                self._run_patch(to_shelve, reverse=True, strip=1)
0.1.94 by Michael Ellerman
Support for 0.7 format diffs when shelving, and better error handling
242
            except PatchFailed:
243
                raise CommandError("Failed removing shelved changes from the"
244
                    "working tree!")
0.1.75 by Michael Ellerman
Update for -p1 format diffs, steal some of Aaron's run_patch() from bzrtools.
245
0.1.107 by Michael Ellerman
Assume -p0 format diffs by default, fall back to -p1 on failure.
246
    def _run_patch(self, patches, strip=0, reverse=False, dry_run=False):
610 by Aaron Bentley
Unify patch invocation
247
        run_patch(self.base, patches, strip, reverse, dry_run)
0.1.87 by Michael Ellerman
Add support for detecting and upgrading from old format shelves.
248
249
    def _check_upgrade(self):
250
        if len(self._list_old_shelves()) > 0:
251
            raise CommandError("Old format shelves found, either upgrade " \
252
                    "or remove them!")
253
254
    def _list_old_shelves(self):
255
        import glob
256
        stem = os.path.join(self.base, '.bzr-shelf')
257
258
        patches = glob.glob(stem)
259
        patches.extend(glob.glob(stem + '-*[!~]'))
260
261
        if len(patches) == 0:
262
            return []
263
264
        def patch_index(name):
265
            if name == stem:
266
                return 0
267
            return int(name[len(stem) + 1:])
268
269
        # patches might not be sorted in the right order
270
        patch_ids = []
271
        for patch in patches:
272
            if patch == stem:
273
                patch_ids.append(0)
274
            else:
275
                patch_ids.append(int(patch[len(stem) + 1:]))
276
277
        patch_ids.sort()
278
279
        patches = []
280
        for id in patch_ids:
281
            if id == 0:
282
                patches.append(stem)
283
            else:
284
                patches.append('%s-%s' % (stem, id))
285
286
        return patches
287
288
    def upgrade(self):
289
        patches = self._list_old_shelves()
290
291
        if len(patches) == 0:
292
            self.log('No old-style shelves found to upgrade.\n')
293
            return
294
295
        for patch in patches:
296
            old_file = open(patch, 'r')
297
            new_path = self.next_patch()
298
            new_file = open(new_path, 'w')
299
            new_file.write(old_file.read())
300
            old_file.close()
301
            new_file.close()
302
            self.log('Copied %s to %s/%s\n' % (os.path.basename(patch),
303
                self.name, os.path.basename(new_path)))
0.6.1 by Michael Ellerman
Win32 fixes from Alexander, slightly reworked by me.
304
            rename(patch, patch + '~')