~abentley/bzrtools/bzrtools.dev

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