~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to shelf.py

  • Committer: Aaron Bentley
  • Date: 2005-09-30 15:28:45 UTC
  • Revision ID: abentley@panoramicfeedback.com-20050930152845-548b00238421d05b
Default-ignored shelf files

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