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