~abentley/bzrtools/bzrtools.dev

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