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