~abentley/bzrtools/bzrtools.dev

86 by Aaron Bentley
Added Michael Ellerman's shelf/unshelf
1
#!/usr/bin/python
2
108 by Aaron Bentley
Updated shelve/unshelve to upstream latest
3
from patches import parse_patches
86 by Aaron Bentley
Added Michael Ellerman's shelf/unshelf
4
import os
5
import sys
6
import string
108 by Aaron Bentley
Updated shelve/unshelve to upstream latest
7
import tty, termios
110 by Aaron Bentley
Added multiple-shelf functionality
8
import glob
86 by Aaron Bentley
Added Michael Ellerman's shelf/unshelf
9
from bzrlib.commands import Command
10
108 by Aaron Bentley
Updated shelve/unshelve to upstream latest
11
def main(args):
12
    name = os.path.basename(args.pop(0))
13
14
    if name not in ['shelve', 'unshelve']:
15
        raise Exception("Unknown command name '%s'" % name)
16
17
    if len(args) > 0:
18
        if args[0] == '--bzr-usage':
19
            print '\n'
20
            return 0
21
        elif args[0] == '--bzr-help':
22
            print 'Shelve a patch, you can get it back later with unshelve.'
23
            return 0
24
        else:
25
            raise Exception("Don't understand args %s" % args)
26
27
    if eval(name + "()"):
28
        return 0
29
30
    return 1
86 by Aaron Bentley
Added Michael Ellerman's shelf/unshelf
31
109 by Aaron Bentley
Shelve: Abstracted out filename selection and root-finding
32
def tree_root():
33
    return run_bzr('root')[0].strip()
34
110 by Aaron Bentley
Added multiple-shelf functionality
35
def shelf_suffix(index):
36
    if index == 0:
37
        return ""
38
    else:
39
        return "-%d" % index
40
109 by Aaron Bentley
Shelve: Abstracted out filename selection and root-finding
41
def next_shelf():
110 by Aaron Bentley
Added multiple-shelf functionality
42
    def name_sequence():
43
        i = 0
44
        while True:
45
            yield shelf_suffix(i)
46
            i = i + 1
47
48
    stem = os.path.join(tree_root(), '.bzr-shelf')
49
    for end in name_sequence():
50
        name = stem + end
51
        if not os.path.exists(name):
52
            return name
109 by Aaron Bentley
Shelve: Abstracted out filename selection and root-finding
53
54
def last_shelf():
110 by Aaron Bentley
Added multiple-shelf functionality
55
    stem = os.path.join(tree_root(), '.bzr-shelf')
56
    shelves = glob.glob(stem)
57
    shelves.extend(glob.glob(stem + '-*'))
58
    def shelf_index(name):
59
        if name == stem:
60
            return 0
61
        return int(name[len(stem)+1:])
62
    shelvenums = [shelf_index(f) for f in shelves]
63
    shelvenums.sort()
64
65
    if len(shelvenums) == 0:
66
        return None
67
    return stem + shelf_suffix(shelvenums[-1])
109 by Aaron Bentley
Shelve: Abstracted out filename selection and root-finding
68
111 by Aaron Bentley
Added support for shelve messages
69
def get_shelf_message(shelf):
70
    prefix = "# shelf: "
71
    if not shelf.startswith(prefix):
72
        return None
73
    return shelf[len(prefix):shelf.index('\n')]
74
86 by Aaron Bentley
Added Michael Ellerman's shelf/unshelf
75
def unshelve():
109 by Aaron Bentley
Shelve: Abstracted out filename selection and root-finding
76
    shelf = last_shelf()
86 by Aaron Bentley
Added Michael Ellerman's shelf/unshelf
77
110 by Aaron Bentley
Added multiple-shelf functionality
78
    if shelf is None:
79
        raise Exception("No shelf found in '%s'" % tree_root())
86 by Aaron Bentley
Added Michael Ellerman's shelf/unshelf
80
81
    patch = open(shelf, 'r').read()
82
111 by Aaron Bentley
Added support for shelve messages
83
    print >>sys.stderr, "Reapplying shelved patches",
84
    message = get_shelf_message(patch)
85
    if message is not None:
86
        print >>sys.stderr, ' "%s"' % message
87
    else:
88
        print >>sys.stderr, ""
109 by Aaron Bentley
Shelve: Abstracted out filename selection and root-finding
89
    pipe = os.popen('patch -d %s -s -p0' % tree_root(), 'w')
86 by Aaron Bentley
Added Michael Ellerman's shelf/unshelf
90
    pipe.write(patch)
91
    pipe.flush()
92
93
    if pipe.close() is not None:
94
        raise Exception("Failed running patch!")
95
96
    os.remove(shelf)
97
    print 'Diff status is now:'
98
    os.system('bzr diff | diffstat')
99
100
    return True
101
108 by Aaron Bentley
Updated shelve/unshelve to upstream latest
102
class QuitException(Exception):
103
    pass
104
113 by aaron.bentley at utoronto
Added -r and file list support to shelve
105
def shelve(message = None, revision = None, file_list = None):
106
    cmd = ['diff']
107
    if revision is not None:
108
        cmd.extend(['--revision', str(revision[0])])
109
    if file_list is not None:
110
        cmd.extend(file_list)
111
    patches = parse_patches(run_bzr(cmd))
108 by Aaron Bentley
Updated shelve/unshelve to upstream latest
112
    try:
113
        patches = HunkSelector(patches).select()
114
    except QuitException:
115
        return False
86 by Aaron Bentley
Added Michael Ellerman's shelf/unshelf
116
108 by Aaron Bentley
Updated shelve/unshelve to upstream latest
117
    if len(patches) == 0:
86 by Aaron Bentley
Added Michael Ellerman's shelf/unshelf
118
        print >>sys.stderr, 'Nothing to shelve'
119
        return True
120
109 by Aaron Bentley
Shelve: Abstracted out filename selection and root-finding
121
    shelf = next_shelf()
86 by Aaron Bentley
Added Michael Ellerman's shelf/unshelf
122
    print >>sys.stderr, "Saving shelved patches to", shelf
123
    shelf = open(shelf, 'a')
111 by Aaron Bentley
Added support for shelve messages
124
    if message is not None:
125
        assert '\n' not in message
126
        shelf.write("# shelf: %s\n" % message)
108 by Aaron Bentley
Updated shelve/unshelve to upstream latest
127
    for patch in patches:
86 by Aaron Bentley
Added Michael Ellerman's shelf/unshelf
128
        shelf.write(str(patch))
129
130
    shelf.flush()
131
    os.fsync(shelf.fileno())
132
    shelf.close()
133
134
    print >>sys.stderr, "Reverting shelved patches"
109 by Aaron Bentley
Shelve: Abstracted out filename selection and root-finding
135
    pipe = os.popen('patch -d %s -sR -p0' % tree_root(), 'w')
108 by Aaron Bentley
Updated shelve/unshelve to upstream latest
136
    for patch in patches:
86 by Aaron Bentley
Added Michael Ellerman's shelf/unshelf
137
       pipe.write(str(patch))
138
    pipe.flush()
139
140
    if pipe.close() is not None:
141
        raise Exception("Failed running patch!")
142
143
    print 'Diff status is now:'
144
    os.system('bzr diff | diffstat')
145
146
    return True
147
148
def run_bzr(args):
149
    if type(args) is str:
150
        args = [ args ]
151
    pipe = os.popen('bzr %s' % string.join(args, ' '), 'r')
152
    lines = pipe.readlines()
153
    if pipe.close() is not None:
154
        raise Exception("Failed running bzr")
155
    return lines
156
157
class cmd_shelve(Command):
158
    """Temporarily remove some changes from the current tree.
159
    Use 'unshelve' to restore these changes.
113 by aaron.bentley at utoronto
Added -r and file list support to shelve
160
161
    If filenames are specified, only changes to those files will be unshelved.
162
    If a revision is specified, all changes since that revision will may be
163
    unshelved.
86 by Aaron Bentley
Added Michael Ellerman's shelf/unshelf
164
    """
114 by aaron.bentley at utoronto
Made files for shelf optional
165
    takes_args = ['file*']
113 by aaron.bentley at utoronto
Added -r and file list support to shelve
166
    takes_options = ['message', 'revision']
167
    def run(self, file_list=None, message=None, revision=None):
168
        return shelve(message=message, file_list=file_list, revision=revision)
86 by Aaron Bentley
Added Michael Ellerman's shelf/unshelf
169
170
class cmd_unshelve(Command):
171
    """Restore previously-shelved changes to the current tree.
172
    See also 'shelve'.
173
    """
174
    def run(self):
175
        return unshelve()
108 by Aaron Bentley
Updated shelve/unshelve to upstream latest
176
177
class HunkSelector:
178
    class Option:
179
        def __init__(self, char, action, help, default=False):
180
            self.char = char
181
            self.action = action
182
            self.default = default
183
            self.help = help
184
185
    standard_options = [
186
        Option('n', 'shelve', 'shelve this change for the moment.',
187
            default=True),
188
        Option('y', 'keep', 'keep this change in your tree.'),
189
        Option('d', 'done', 'done, skip to the end.'),
190
        Option('i', 'invert', 'invert the current selection of all hunks.'),
191
        Option('s', 'status', 'show status of hunks.'),
192
        Option('q', 'quit', 'quit')
193
    ]
194
195
    end_options = [
196
        Option('y', 'continue', 'proceed to shelve selected changes.',
197
            default=True),
198
        Option('r', 'restart', 'restart the hunk selection loop.'),
199
        Option('s', 'status', 'show status of hunks.'),
200
        Option('i', 'invert', 'invert the current selection of all hunks.'),
201
        Option('q', 'quit', 'quit')
202
    ]
203
204
    def __init__(self, patches):
205
        self.patches = patches
206
        self.total_hunks = 0
207
        for patch in patches:
208
            for hunk in patch.hunks:
209
                # everything's shelved by default
210
                hunk.selected = True
211
                self.total_hunks += 1
212
213
    def __get_option(self, char):
214
        for opt in self.standard_options:
215
            if opt.char == char:
216
                return opt
217
        raise Exception('Option "%s" not found!' % char)
218
219
    def __select_loop(self):
220
        j = 0
221
        for patch in self.patches:
222
            i = 0
223
            lasti = -1
224
            while i < len(patch.hunks):
225
                hunk = patch.hunks[i]
226
                if lasti != i:
227
                    print patch.get_header(), hunk
228
                    j += 1
229
                lasti = i
230
231
                prompt = 'Keep this change? (%d of %d)' \
232
                            % (j, self.total_hunks)
233
234
                if hunk.selected:
235
                    self.__get_option('n').default = True
236
                    self.__get_option('y').default = False
237
                else:
238
                    self.__get_option('n').default = False
239
                    self.__get_option('y').default = True
240
241
                action = self.__ask_user(prompt, self.standard_options)
242
243
                if action == 'keep':
244
                    hunk.selected = False
245
                elif action == 'shelve':
246
                    hunk.selected = True
247
                elif action == 'done':
248
                    return True
249
                elif action == 'invert':
250
                    self.__invert_selection()
251
                    self.__show_status()
252
                    continue
253
                elif action == 'status':
254
                    self.__show_status()
255
                    continue
256
                elif action == 'quit':
257
                    return False
258
259
                i += 1
260
        return True
261
262
    def select(self):
263
        if self.total_hunks == 0:
264
            return []
265
266
        done = False
267
        while not done:
268
            if not self.__select_loop():
269
                return []
270
271
            while True:
272
                self.__show_status()
273
                prompt = "Shelve these changes, or restart?"
274
                action = self.__ask_user(prompt, self.end_options)
275
276
                if action == 'continue':
277
                    done = True
278
                    break
279
                elif action == 'quit':
280
                    return []
281
                elif action == 'status':
282
                    self.__show_status()
283
                elif action == 'invert':
284
                    self.__invert_selection()
285
                elif action == 'restart':
286
                    break
287
288
289
        for patch in self.patches:
290
            tmp = []
291
            for hunk in patch.hunks:
292
                if hunk.selected:
293
                    tmp.append(hunk)
294
            patch.hunks = tmp
295
296
        tmp = []
297
        for patch in self.patches:
298
            if len(patch.hunks):
299
                tmp.append(patch)
300
        self.patches = tmp
301
302
        return self.patches
303
304
    def __invert_selection(self):
305
        for patch in self.patches:
306
            for hunk in patch.hunks:
307
                if hunk.__dict__.has_key('selected'):
308
                    hunk.selected = not hunk.selected
309
                else:
310
                    hunk.selected = True
311
312
    def __show_status(self):
313
        print '\nStatus:'
314
        for patch in self.patches:
315
            print '  %s' % patch.oldname
316
            shelve = 0
317
            keep = 0
318
            for hunk in patch.hunks:
319
                if hunk.selected:
320
                    shelve += 1
321
                else:
322
                    keep += 1
323
324
            print '    %d hunks to be shelved' % shelve
325
            print '    %d hunks to be kept' % keep
326
            print
327
328
    def __getchar(self):
329
        fd = sys.stdin.fileno()
330
        settings = termios.tcgetattr(fd)
331
        try:
332
            tty.setraw(fd)
333
            ch = sys.stdin.read(1)
334
        finally:
335
            termios.tcsetattr(fd, termios.TCSADRAIN, settings)
336
        return ch
337
338
    def __ask_user(self, prompt, options):
339
        while True:
340
            sys.stdout.write(prompt)
341
            sys.stdout.write(' [')
342
            for opt in options:
343
                if opt.default:
344
                    default = opt
345
                sys.stdout.write(opt.char)
346
            sys.stdout.write('?] (%s): ' % default.char)
347
348
            response = self.__getchar()
349
350
            # default, which we see as newline, is 'n'
351
            if response in ['\n', '\r', '\r\n']:
352
                response = default.char
353
354
            print response # because echo is off
355
356
            for opt in options:
357
                if opt.char == response:
358
                    return opt.action
359
360
            for opt in options:
361
                print '  %s - %s' % (opt.char, opt.help)