~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to shelf.py

  • Committer: Michael Ellerman
  • Date: 2005-10-19 10:11:42 UTC
  • mto: (0.3.1 shelf-dev) (325.1.2 bzrtools)
  • mto: This revision was merged to the branch mainline in revision 246.
  • Revision ID: michael@ellerman.id.au-20051019101142-544d03b0b5c0cb96
Split out HunkSelector class.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
 
 
3
from patches import parse_patches
1
4
import os
2
5
import sys
3
 
import subprocess
4
 
from datetime import datetime
5
 
from errors import CommandError, PatchFailed, PatchInvokeError
6
 
from hunk_selector import ShelveHunkSelector, UnshelveHunkSelector
7
 
from patchsource import PatchSource, FilePatchSource
8
 
from bzrlib.osutils import rename
9
 
 
10
 
class Shelf(object):
11
 
    MESSAGE_PREFIX = "# Shelved patch: "
12
 
 
13
 
    _paths = {
14
 
        'base'          : '.shelf',
15
 
        'shelves'       : '.shelf/shelves',
16
 
        'current-shelf' : '.shelf/current-shelf',
17
 
    }
18
 
 
19
 
    def __init__(self, base, name=None):
20
 
        self.base = base
21
 
        self.__setup()
22
 
 
23
 
        if name is None:
24
 
            current = os.path.join(self.base, self._paths['current-shelf'])
25
 
            name = open(current).read().strip()
26
 
 
27
 
        assert '\n' not in name
28
 
        self.name = name
29
 
 
30
 
        self.dir = os.path.join(self.base, self._paths['shelves'], name)
31
 
        if not os.path.isdir(self.dir):
32
 
            os.mkdir(self.dir)
33
 
 
34
 
    def __setup(self):
35
 
        # Create required directories etc.
36
 
        for dir in [self._paths['base'], self._paths['shelves']]:
37
 
            dir = os.path.join(self.base, dir)
38
 
            if not os.path.isdir(dir):
39
 
                os.mkdir(dir)
40
 
 
41
 
        current = os.path.join(self.base, self._paths['current-shelf'])
42
 
        if not os.path.exists(current):
43
 
            f = open(current, 'w')
44
 
            f.write('default')
45
 
            f.close()
46
 
 
47
 
    def make_default(self):
48
 
        f = open(os.path.join(self.base, self._paths['current-shelf']), 'w')
49
 
        f.write(self.name)
50
 
        f.close()
51
 
        self.log("Default shelf is now '%s'\n" % self.name)
52
 
 
53
 
    def log(self, msg):
54
 
        sys.stderr.write(msg)
55
 
 
56
 
    def delete(self, patch):
57
 
        path = self.__path_from_user(patch)
58
 
        rename(path, '%s~' % path)
59
 
 
60
 
    def display(self, patch=None):
61
 
        if patch is None:
62
 
            path = self.last_patch()
63
 
        else:
64
 
            path = self.__path_from_user(patch)
65
 
        sys.stdout.write(open(path).read())
66
 
 
67
 
    def list(self):
68
 
        indexes = self.__list()
69
 
        self.log("Patches on shelf '%s':" % self.name)
70
 
        if len(indexes) == 0:
71
 
            self.log(' None\n')
72
 
            return
73
 
        self.log('\n')
74
 
        for index in indexes:
75
 
            msg = self.get_patch_message(self.__path(index))
76
 
            if msg is None:
77
 
                msg = "No message saved with patch."
78
 
            self.log(' %.2d: %s\n' % (index, msg))
79
 
 
80
 
    def __path_from_user(self, patch_id):
81
 
        try:
82
 
            patch_index = int(patch_id)
83
 
        except (TypeError, ValueError):
84
 
            raise CommandError("Invalid patch name '%s'" % patch_id)
85
 
 
86
 
        path = self.__path(patch_index)
87
 
 
88
 
        if not os.path.exists(path):
89
 
            raise CommandError("Patch '%s' doesn't exist on shelf %s!" % \
90
 
                        (patch_id, self.name))
91
 
 
92
 
        return path
93
 
 
94
 
    def __path(self, index):
95
 
        return os.path.join(self.dir, '%.2d' % index)
96
 
 
97
 
    def next_patch(self):
98
 
        indexes = self.__list()
99
 
 
100
 
        if len(indexes) == 0:
101
 
            next = 0
102
 
        else:
103
 
            next = indexes[-1] + 1
104
 
        return self.__path(next)
105
 
 
106
 
    def __list(self):
107
 
        patches = os.listdir(self.dir)
108
 
        indexes = []
109
 
        for f in patches:
110
 
            if f.endswith('~'):
111
 
                continue # ignore backup files
112
 
            try:
113
 
                indexes.append(int(f))
114
 
            except ValueError:
115
 
                self.log("Warning: Ignoring junk file '%s' on shelf.\n" % f)
116
 
 
117
 
        indexes.sort()
118
 
        return indexes
119
 
 
120
 
    def last_patch(self):
121
 
        indexes = self.__list()
122
 
 
123
 
        if len(indexes) == 0:
124
 
            return None
125
 
 
126
 
        return self.__path(indexes[-1])
127
 
 
128
 
    def get_patch_message(self, patch_path):
129
 
        patch = open(patch_path, 'r').read()
130
 
 
131
 
        if not patch.startswith(self.MESSAGE_PREFIX):
132
 
            return None
133
 
        return patch[len(self.MESSAGE_PREFIX):patch.index('\n')]
134
 
 
135
 
    def unshelve(self, patch_source, patch_name=None, all=False, force=False,
136
 
                 no_color=False):
137
 
        self._check_upgrade()
138
 
 
139
 
        if no_color is False:
140
 
            color = None
141
 
        else:
142
 
            color = False
143
 
        if patch_name is None:
144
 
            patch_path = self.last_patch()
145
 
        else:
146
 
            patch_path = self.__path_from_user(patch_name)
147
 
 
148
 
        if patch_path is None:
149
 
            raise CommandError("No patch found on shelf %s" % self.name)
150
 
 
151
 
        patches = FilePatchSource(patch_path).readpatches()
152
 
        if all:
153
 
            to_unshelve = patches
154
 
            to_remain = []
155
 
        else:
156
 
            hs = UnshelveHunkSelector(patches, color)
157
 
            to_unshelve, to_remain = hs.select()
158
 
 
159
 
        if len(to_unshelve) == 0:
160
 
            raise CommandError('Nothing to unshelve')
161
 
 
162
 
        message = self.get_patch_message(patch_path)
163
 
        if message is None:
164
 
            message = "No message saved with patch."
165
 
        self.log('Unshelving from %s/%s: "%s"\n' % \
166
 
                (self.name, os.path.basename(patch_path), message))
167
 
 
168
 
        try:
169
 
            self._run_patch(to_unshelve, dry_run=True)
170
 
            self._run_patch(to_unshelve)
171
 
        except PatchFailed:
172
 
            try:
173
 
                self._run_patch(to_unshelve, strip=1, dry_run=True)
174
 
                self._run_patch(to_unshelve, strip=1)
175
 
            except PatchFailed:
176
 
                if force:
177
 
                    self.log('Warning: Unshelving failed, forcing as ' \
178
 
                             'requested. Shelf will not be modified.\n')
179
 
                    try:
180
 
                        self._run_patch(to_unshelve)
181
 
                    except PatchFailed:
182
 
                        pass
183
 
                    return
184
 
                raise CommandError("Your shelved patch no " \
185
 
                    "longer applies cleanly to the working tree!")
186
 
 
187
 
        # Backup the shelved patch
188
 
        rename(patch_path, '%s~' % patch_path)
189
 
 
190
 
        if len(to_remain) > 0:
191
 
            f = open(patch_path, 'w')
192
 
            for patch in to_remain:
193
 
                f.write(str(patch))
194
 
            f.close()
195
 
 
196
 
    def shelve(self, patch_source, all=False, message=None, no_color=False):
197
 
        self._check_upgrade()
198
 
        if no_color is False:
199
 
            color = None
200
 
        else:
201
 
            color = False
202
 
 
203
 
        patches = patch_source.readpatches()
204
 
 
205
 
        if all:
206
 
            to_shelve = patches
207
 
        else:
208
 
            to_shelve = ShelveHunkSelector(patches, color).select()[0]
209
 
 
210
 
        if len(to_shelve) == 0:
211
 
            raise CommandError('Nothing to shelve')
212
 
 
213
 
        if message is None:
214
 
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
215
 
            message = "Changes shelved on %s" % timestamp
216
 
 
217
 
        patch_path = self.next_patch()
218
 
        self.log('Shelving to %s/%s: "%s"\n' % \
219
 
                (self.name, os.path.basename(patch_path), message))
220
 
 
221
 
        f = open(patch_path, 'a')
222
 
 
223
 
        assert '\n' not in message
224
 
        f.write("%s%s\n" % (self.MESSAGE_PREFIX, message))
225
 
 
226
 
        for patch in to_shelve:
227
 
            f.write(str(patch))
228
 
 
229
 
        f.flush()
230
 
        os.fsync(f.fileno())
231
 
        f.close()
232
 
 
233
 
        try:
234
 
            self._run_patch(to_shelve, reverse=True, dry_run=True)
235
 
            self._run_patch(to_shelve, reverse=True)
236
 
        except PatchFailed:
237
 
            try:
238
 
                self._run_patch(to_shelve, reverse=True, strip=1, dry_run=True)
239
 
                self._run_patch(to_shelve, reverse=True, strip=1)
240
 
            except PatchFailed:
241
 
                raise CommandError("Failed removing shelved changes from the"
242
 
                    "working tree!")
243
 
 
244
 
    def _run_patch(self, patches, strip=0, reverse=False, dry_run=False):
245
 
        args = ['patch', '-d', self.base, '-s', '-p%d' % strip, '-f']
246
 
 
247
 
        if sys.platform == "win32":
248
 
            args.append('--binary')
249
 
 
250
 
        if reverse:
251
 
            args.append('-R')
252
 
        if dry_run:
253
 
            args.append('--dry-run')
254
 
            stdout = stderr = subprocess.PIPE
255
 
        else:
256
 
            stdout = stderr = None
257
 
 
258
 
        try:
259
 
            process = subprocess.Popen(args, stdin=subprocess.PIPE,
260
 
                                       stdout=stdout, stderr=stderr)
261
 
            for patch in patches:
262
 
                process.stdin.write(str(patch))
263
 
 
264
 
        except IOError, e:
265
 
            raise PatchInvokeError(e, process.stderr.read())
266
 
 
267
 
        process.communicate()
268
 
 
269
 
        result = process.wait()
270
 
        if result != 0:
271
 
            raise PatchFailed()
272
 
 
273
 
        return result
274
 
 
275
 
    def _check_upgrade(self):
276
 
        if len(self._list_old_shelves()) > 0:
277
 
            raise CommandError("Old format shelves found, either upgrade " \
278
 
                    "or remove them!")
279
 
 
280
 
    def _list_old_shelves(self):
281
 
        import glob
282
 
        stem = os.path.join(self.base, '.bzr-shelf')
283
 
 
284
 
        patches = glob.glob(stem)
285
 
        patches.extend(glob.glob(stem + '-*[!~]'))
286
 
 
287
 
        if len(patches) == 0:
 
6
import string
 
7
import tty, termios
 
8
 
 
9
def main(args):
 
10
    name = os.path.basename(args.pop(0))
 
11
 
 
12
    if name not in ['shelve', 'unshelve']:
 
13
        raise Exception("Unknown command name '%s'" % name)
 
14
 
 
15
    if len(args) > 0:
 
16
        if args[0] == '--bzr-usage':
 
17
            print '\n'
 
18
            return 0
 
19
        elif args[0] == '--bzr-help':
 
20
            print 'Shelve a patch, you can get it back later with unshelve.'
 
21
            return 0
 
22
        else:
 
23
            raise Exception("Don't understand args %s" % args)
 
24
 
 
25
    if eval(name + "()"):
 
26
        return 0
 
27
 
 
28
    return 1
 
29
 
 
30
def unshelve():
 
31
    root = run_bzr('root')[0].strip()
 
32
    shelf = os.path.join(root, '.bzr-shelf')
 
33
 
 
34
    if not os.path.exists(shelf):
 
35
        raise Exception("No shelf found in '%s'" % shelf)
 
36
 
 
37
    patch = open(shelf, 'r').read()
 
38
 
 
39
    print >>sys.stderr, "Reapplying shelved patches"
 
40
    pipe = os.popen('patch -d %s -s -p0' % root, 'w')
 
41
    pipe.write(patch)
 
42
    pipe.flush()
 
43
 
 
44
    if pipe.close() is not None:
 
45
        raise Exception("Failed running patch!")
 
46
 
 
47
    os.remove(shelf)
 
48
    print 'Diff status is now:'
 
49
    os.system('bzr diff | diffstat')
 
50
 
 
51
    return True
 
52
 
 
53
class QuitException(Exception):
 
54
    pass
 
55
 
 
56
def shelve():
 
57
    patches = parse_patches(run_bzr('diff'))
 
58
    try:
 
59
        patches = HunkSelector(patches).select()
 
60
    except QuitException:
 
61
        return False
 
62
 
 
63
    if len(patches) == 0:
 
64
        print >>sys.stderr, 'Nothing to shelve'
 
65
        return True
 
66
 
 
67
    root = run_bzr('root')[0].strip()
 
68
    shelf = os.path.join(root, '.bzr-shelf')
 
69
    print >>sys.stderr, "Saving shelved patches to", shelf
 
70
    shelf = open(shelf, 'a')
 
71
 
 
72
    for patch in patches:
 
73
        shelf.write(str(patch))
 
74
 
 
75
    shelf.flush()
 
76
    os.fsync(shelf.fileno())
 
77
    shelf.close()
 
78
 
 
79
    print >>sys.stderr, "Reverting shelved patches"
 
80
    pipe = os.popen('patch -d %s -sR -p0' % root, 'w')
 
81
    for patch in patches:
 
82
       pipe.write(str(patch))
 
83
    pipe.flush()
 
84
 
 
85
    if pipe.close() is not None:
 
86
        raise Exception("Failed running patch!")
 
87
 
 
88
    print 'Diff status is now:'
 
89
    os.system('bzr diff | diffstat')
 
90
 
 
91
    return True
 
92
 
 
93
def run_bzr(args):
 
94
    if type(args) is str:
 
95
        args = [ args ]
 
96
    pipe = os.popen('bzr %s' % string.join(args, ' '), 'r')
 
97
    lines = pipe.readlines()
 
98
    if pipe.close() is not None:
 
99
        raise Exception("Failed running bzr")
 
100
    return lines
 
101
 
 
102
 
 
103
class HunkSelector:
 
104
    class Option:
 
105
        def __init__(self, char, action, help, default=False):
 
106
            self.char = char
 
107
            self.action = action
 
108
            self.default = default
 
109
            self.help = help
 
110
 
 
111
    standard_options = [
 
112
        Option('n', 'shelve', 'shelve this change for the moment.',
 
113
            default=True),
 
114
        Option('y', 'keep', 'keep this change in your tree.'),
 
115
        Option('d', 'done', 'done, skip to the end.'),
 
116
        Option('i', 'invert', 'invert the current selection of all hunks.'),
 
117
        Option('s', 'status', 'show status of hunks.'),
 
118
        Option('q', 'quit', 'quit')
 
119
    ]
 
120
 
 
121
    end_options = [
 
122
        Option('y', 'continue', 'proceed to shelve selected changes.',
 
123
            default=True),
 
124
        Option('r', 'restart', 'restart the hunk selection loop.'),
 
125
        Option('s', 'status', 'show status of hunks.'),
 
126
        Option('i', 'invert', 'invert the current selection of all hunks.'),
 
127
        Option('q', 'quit', 'quit')
 
128
    ]
 
129
 
 
130
    def __init__(self, patches):
 
131
        self.patches = patches
 
132
        self.total_hunks = 0
 
133
        for patch in patches:
 
134
            for hunk in patch.hunks:
 
135
                # everything's shelved by default
 
136
                hunk.selected = True
 
137
                self.total_hunks += 1
 
138
 
 
139
    def __get_option(self, char):
 
140
        for opt in self.standard_options:
 
141
            if opt.char == char:
 
142
                return opt
 
143
        raise Exception('Option "%s" not found!' % char)
 
144
 
 
145
    def __select_loop(self):
 
146
        j = 0
 
147
        for patch in self.patches:
 
148
            i = 0
 
149
            lasti = -1
 
150
            while i < len(patch.hunks):
 
151
                hunk = patch.hunks[i]
 
152
                if lasti != i:
 
153
                    print patch.get_header(), hunk
 
154
                    j += 1
 
155
                lasti = i
 
156
 
 
157
                prompt = 'Keep this change? (%d of %d)' \
 
158
                            % (j, self.total_hunks)
 
159
 
 
160
                if hunk.selected:
 
161
                    self.__get_option('n').default = True
 
162
                    self.__get_option('y').default = False
 
163
                else:
 
164
                    self.__get_option('n').default = False
 
165
                    self.__get_option('y').default = True
 
166
 
 
167
                action = self.__ask_user(prompt, self.standard_options)
 
168
 
 
169
                if action == 'keep':
 
170
                    hunk.selected = False
 
171
                elif action == 'shelve':
 
172
                    hunk.selected = True
 
173
                elif action == 'done':
 
174
                    return True
 
175
                elif action == 'invert':
 
176
                    self.__invert_selection()
 
177
                    self.__show_status()
 
178
                    continue
 
179
                elif action == 'status':
 
180
                    self.__show_status()
 
181
                    continue
 
182
                elif action == 'quit':
 
183
                    return False
 
184
 
 
185
                i += 1
 
186
        return True
 
187
 
 
188
    def select(self):
 
189
        if self.total_hunks == 0:
288
190
            return []
289
191
 
290
 
        def patch_index(name):
291
 
            if name == stem:
292
 
                return 0
293
 
            return int(name[len(stem) + 1:])
294
 
 
295
 
        # patches might not be sorted in the right order
296
 
        patch_ids = []
297
 
        for patch in patches:
298
 
            if patch == stem:
299
 
                patch_ids.append(0)
300
 
            else:
301
 
                patch_ids.append(int(patch[len(stem) + 1:]))
302
 
 
303
 
        patch_ids.sort()
304
 
 
305
 
        patches = []
306
 
        for id in patch_ids:
307
 
            if id == 0:
308
 
                patches.append(stem)
309
 
            else:
310
 
                patches.append('%s-%s' % (stem, id))
311
 
 
312
 
        return patches
313
 
 
314
 
    def upgrade(self):
315
 
        patches = self._list_old_shelves()
316
 
 
317
 
        if len(patches) == 0:
318
 
            self.log('No old-style shelves found to upgrade.\n')
319
 
            return
320
 
 
321
 
        for patch in patches:
322
 
            old_file = open(patch, 'r')
323
 
            new_path = self.next_patch()
324
 
            new_file = open(new_path, 'w')
325
 
            new_file.write(old_file.read())
326
 
            old_file.close()
327
 
            new_file.close()
328
 
            self.log('Copied %s to %s/%s\n' % (os.path.basename(patch),
329
 
                self.name, os.path.basename(new_path)))
330
 
            rename(patch, patch + '~')
 
192
        done = False
 
193
        while not done:
 
194
            if not self.__select_loop():
 
195
                return []
 
196
 
 
197
            while True:
 
198
                self.__show_status()
 
199
                prompt = "Shelve these changes, or restart?"
 
200
                action = self.__ask_user(prompt, self.end_options)
 
201
 
 
202
                if action == 'continue':
 
203
                    done = True
 
204
                    break
 
205
                elif action == 'quit':
 
206
                    return []
 
207
                elif action == 'status':
 
208
                    self.__show_status()
 
209
                elif action == 'invert':
 
210
                    self.__invert_selection()
 
211
                elif action == 'restart':
 
212
                    break
 
213
 
 
214
 
 
215
        for patch in self.patches:
 
216
            tmp = []
 
217
            for hunk in patch.hunks:
 
218
                if hunk.selected:
 
219
                    tmp.append(hunk)
 
220
            patch.hunks = tmp
 
221
 
 
222
        tmp = []
 
223
        for patch in self.patches:
 
224
            if len(patch.hunks):
 
225
                tmp.append(patch)
 
226
        self.patches = tmp
 
227
 
 
228
        return self.patches
 
229
 
 
230
    def __invert_selection(self):
 
231
        for patch in self.patches:
 
232
            for hunk in patch.hunks:
 
233
                if hunk.__dict__.has_key('selected'):
 
234
                    hunk.selected = not hunk.selected
 
235
                else:
 
236
                    hunk.selected = True
 
237
 
 
238
    def __show_status(self):
 
239
        print '\nStatus:'
 
240
        for patch in self.patches:
 
241
            print '  %s' % patch.oldname
 
242
            shelve = 0
 
243
            keep = 0
 
244
            for hunk in patch.hunks:
 
245
                if hunk.selected:
 
246
                    shelve += 1
 
247
                else:
 
248
                    keep += 1
 
249
 
 
250
            print '    %d hunks to be shelved' % shelve
 
251
            print '    %d hunks to be kept' % keep
 
252
            print
 
253
 
 
254
    def __getchar(self):
 
255
        fd = sys.stdin.fileno()
 
256
        settings = termios.tcgetattr(fd)
 
257
        try:
 
258
            tty.setraw(fd)
 
259
            ch = sys.stdin.read(1)
 
260
        finally:
 
261
            termios.tcsetattr(fd, termios.TCSADRAIN, settings)
 
262
        return ch
 
263
 
 
264
    def __ask_user(self, prompt, options):
 
265
        while True:
 
266
            sys.stdout.write(prompt)
 
267
            sys.stdout.write(' [')
 
268
            for opt in options:
 
269
                if opt.default:
 
270
                    default = opt
 
271
                sys.stdout.write(opt.char)
 
272
            sys.stdout.write('?] (%s): ' % default.char)
 
273
 
 
274
            response = self.__getchar()
 
275
 
 
276
            # default, which we see as newline, is 'n'
 
277
            if response in ['\n', '\r', '\r\n']:
 
278
                response = default.char
 
279
 
 
280
            print response # because echo is off
 
281
 
 
282
            for opt in options:
 
283
                if opt.char == response:
 
284
                    return opt.action
 
285
 
 
286
            for opt in options:
 
287
                print '  %s - %s' % (opt.char, opt.help)