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