~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to shelf.py

  • Committer: Aaron Bentley
  • Date: 2005-08-17 18:20:10 UTC
  • Revision ID: abentley@panoramicfeedback.com-20050817182010-427e0938841dd9a8
Cleaned up handling of files with no terminating \n

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
 
 
3
from patches import parse_patches
 
4
import os
 
5
import sys
 
6
import string
 
7
import tty, termios
 
8
import glob
 
9
from bzrlib.commands import Command
 
10
 
 
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
 
31
 
 
32
def tree_root():
 
33
    return run_bzr('root')[0].strip()
 
34
 
 
35
def shelf_suffix(index):
 
36
    if index == 0:
 
37
        return ""
 
38
    else:
 
39
        return "-%d" % index
 
40
 
 
41
def next_shelf():
 
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
 
53
 
 
54
def last_shelf():
 
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])
 
68
 
 
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
 
 
75
def unshelve():
 
76
    shelf = last_shelf()
 
77
 
 
78
    if shelf is None:
 
79
        raise Exception("No shelf found in '%s'" % tree_root())
 
80
 
 
81
    patch = open(shelf, 'r').read()
 
82
 
 
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, ""
 
89
    pipe = os.popen('patch -d %s -s -p0' % tree_root(), 'w')
 
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
 
 
102
class QuitException(Exception):
 
103
    pass
 
104
 
 
105
def shelve(message = None):
 
106
    patches = parse_patches(run_bzr('diff'))
 
107
    try:
 
108
        patches = HunkSelector(patches).select()
 
109
    except QuitException:
 
110
        return False
 
111
 
 
112
    if len(patches) == 0:
 
113
        print >>sys.stderr, 'Nothing to shelve'
 
114
        return True
 
115
 
 
116
    shelf = next_shelf()
 
117
    print >>sys.stderr, "Saving shelved patches to", shelf
 
118
    shelf = open(shelf, 'a')
 
119
    if message is not None:
 
120
        assert '\n' not in message
 
121
        shelf.write("# shelf: %s\n" % message)
 
122
    for patch in patches:
 
123
        shelf.write(str(patch))
 
124
 
 
125
    shelf.flush()
 
126
    os.fsync(shelf.fileno())
 
127
    shelf.close()
 
128
 
 
129
    print >>sys.stderr, "Reverting shelved patches"
 
130
    pipe = os.popen('patch -d %s -sR -p0' % tree_root(), 'w')
 
131
    for patch in patches:
 
132
       pipe.write(str(patch))
 
133
    pipe.flush()
 
134
 
 
135
    if pipe.close() is not None:
 
136
        raise Exception("Failed running patch!")
 
137
 
 
138
    print 'Diff status is now:'
 
139
    os.system('bzr diff | diffstat')
 
140
 
 
141
    return True
 
142
 
 
143
def run_bzr(args):
 
144
    if type(args) is str:
 
145
        args = [ args ]
 
146
    pipe = os.popen('bzr %s' % string.join(args, ' '), 'r')
 
147
    lines = pipe.readlines()
 
148
    if pipe.close() is not None:
 
149
        raise Exception("Failed running bzr")
 
150
    return lines
 
151
 
 
152
class cmd_shelve(Command):
 
153
    """Temporarily remove some changes from the current tree.
 
154
    Use 'unshelve' to restore these changes.
 
155
    """
 
156
    takes_options = ['message']
 
157
    def run(self, message=None):
 
158
        return shelve(message)
 
159
 
 
160
class cmd_unshelve(Command):
 
161
    """Restore previously-shelved changes to the current tree.
 
162
    See also 'shelve'.
 
163
    """
 
164
    def run(self):
 
165
        return unshelve()
 
166
 
 
167
class HunkSelector:
 
168
    class Option:
 
169
        def __init__(self, char, action, help, default=False):
 
170
            self.char = char
 
171
            self.action = action
 
172
            self.default = default
 
173
            self.help = help
 
174
 
 
175
    standard_options = [
 
176
        Option('n', 'shelve', 'shelve this change for the moment.',
 
177
            default=True),
 
178
        Option('y', 'keep', 'keep this change in your tree.'),
 
179
        Option('d', 'done', 'done, skip to the end.'),
 
180
        Option('i', 'invert', 'invert the current selection of all hunks.'),
 
181
        Option('s', 'status', 'show status of hunks.'),
 
182
        Option('q', 'quit', 'quit')
 
183
    ]
 
184
 
 
185
    end_options = [
 
186
        Option('y', 'continue', 'proceed to shelve selected changes.',
 
187
            default=True),
 
188
        Option('r', 'restart', 'restart the hunk selection loop.'),
 
189
        Option('s', 'status', 'show status of hunks.'),
 
190
        Option('i', 'invert', 'invert the current selection of all hunks.'),
 
191
        Option('q', 'quit', 'quit')
 
192
    ]
 
193
 
 
194
    def __init__(self, patches):
 
195
        self.patches = patches
 
196
        self.total_hunks = 0
 
197
        for patch in patches:
 
198
            for hunk in patch.hunks:
 
199
                # everything's shelved by default
 
200
                hunk.selected = True
 
201
                self.total_hunks += 1
 
202
 
 
203
    def __get_option(self, char):
 
204
        for opt in self.standard_options:
 
205
            if opt.char == char:
 
206
                return opt
 
207
        raise Exception('Option "%s" not found!' % char)
 
208
 
 
209
    def __select_loop(self):
 
210
        j = 0
 
211
        for patch in self.patches:
 
212
            i = 0
 
213
            lasti = -1
 
214
            while i < len(patch.hunks):
 
215
                hunk = patch.hunks[i]
 
216
                if lasti != i:
 
217
                    print patch.get_header(), hunk
 
218
                    j += 1
 
219
                lasti = i
 
220
 
 
221
                prompt = 'Keep this change? (%d of %d)' \
 
222
                            % (j, self.total_hunks)
 
223
 
 
224
                if hunk.selected:
 
225
                    self.__get_option('n').default = True
 
226
                    self.__get_option('y').default = False
 
227
                else:
 
228
                    self.__get_option('n').default = False
 
229
                    self.__get_option('y').default = True
 
230
 
 
231
                action = self.__ask_user(prompt, self.standard_options)
 
232
 
 
233
                if action == 'keep':
 
234
                    hunk.selected = False
 
235
                elif action == 'shelve':
 
236
                    hunk.selected = True
 
237
                elif action == 'done':
 
238
                    return True
 
239
                elif action == 'invert':
 
240
                    self.__invert_selection()
 
241
                    self.__show_status()
 
242
                    continue
 
243
                elif action == 'status':
 
244
                    self.__show_status()
 
245
                    continue
 
246
                elif action == 'quit':
 
247
                    return False
 
248
 
 
249
                i += 1
 
250
        return True
 
251
 
 
252
    def select(self):
 
253
        if self.total_hunks == 0:
 
254
            return []
 
255
 
 
256
        done = False
 
257
        while not done:
 
258
            if not self.__select_loop():
 
259
                return []
 
260
 
 
261
            while True:
 
262
                self.__show_status()
 
263
                prompt = "Shelve these changes, or restart?"
 
264
                action = self.__ask_user(prompt, self.end_options)
 
265
 
 
266
                if action == 'continue':
 
267
                    done = True
 
268
                    break
 
269
                elif action == 'quit':
 
270
                    return []
 
271
                elif action == 'status':
 
272
                    self.__show_status()
 
273
                elif action == 'invert':
 
274
                    self.__invert_selection()
 
275
                elif action == 'restart':
 
276
                    break
 
277
 
 
278
 
 
279
        for patch in self.patches:
 
280
            tmp = []
 
281
            for hunk in patch.hunks:
 
282
                if hunk.selected:
 
283
                    tmp.append(hunk)
 
284
            patch.hunks = tmp
 
285
 
 
286
        tmp = []
 
287
        for patch in self.patches:
 
288
            if len(patch.hunks):
 
289
                tmp.append(patch)
 
290
        self.patches = tmp
 
291
 
 
292
        return self.patches
 
293
 
 
294
    def __invert_selection(self):
 
295
        for patch in self.patches:
 
296
            for hunk in patch.hunks:
 
297
                if hunk.__dict__.has_key('selected'):
 
298
                    hunk.selected = not hunk.selected
 
299
                else:
 
300
                    hunk.selected = True
 
301
 
 
302
    def __show_status(self):
 
303
        print '\nStatus:'
 
304
        for patch in self.patches:
 
305
            print '  %s' % patch.oldname
 
306
            shelve = 0
 
307
            keep = 0
 
308
            for hunk in patch.hunks:
 
309
                if hunk.selected:
 
310
                    shelve += 1
 
311
                else:
 
312
                    keep += 1
 
313
 
 
314
            print '    %d hunks to be shelved' % shelve
 
315
            print '    %d hunks to be kept' % keep
 
316
            print
 
317
 
 
318
    def __getchar(self):
 
319
        fd = sys.stdin.fileno()
 
320
        settings = termios.tcgetattr(fd)
 
321
        try:
 
322
            tty.setraw(fd)
 
323
            ch = sys.stdin.read(1)
 
324
        finally:
 
325
            termios.tcsetattr(fd, termios.TCSADRAIN, settings)
 
326
        return ch
 
327
 
 
328
    def __ask_user(self, prompt, options):
 
329
        while True:
 
330
            sys.stdout.write(prompt)
 
331
            sys.stdout.write(' [')
 
332
            for opt in options:
 
333
                if opt.default:
 
334
                    default = opt
 
335
                sys.stdout.write(opt.char)
 
336
            sys.stdout.write('?] (%s): ' % default.char)
 
337
 
 
338
            response = self.__getchar()
 
339
 
 
340
            # default, which we see as newline, is 'n'
 
341
            if response in ['\n', '\r', '\r\n']:
 
342
                response = default.char
 
343
 
 
344
            print response # because echo is off
 
345
 
 
346
            for opt in options:
 
347
                if opt.char == response:
 
348
                    return opt.action
 
349
 
 
350
            for opt in options:
 
351
                print '  %s - %s' % (opt.char, opt.help)