~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to shelf.py

  • Committer: Michael Ellerman
  • Date: 2005-11-29 07:12:26 UTC
  • mto: (0.3.1 shelf-dev) (325.1.2 bzrtools)
  • mto: This revision was merged to the branch mainline in revision 334.
  • Revision ID: michael@ellerman.id.au-20051129071226-a04b3f827880025d
Unshelve --pick was broken, because we deleted the whole patch, even when only
part of it was unshelved. So now if we unshelve part of a patch, the patch is
replaced with a new patch that has just the unshelved parts. That's a long way
of saying it does what you'd expect.

Implementing this required changing HunkSelector to return both the selected,
and unselected hunks (ie. patches to shelve, and patches to keep).

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python
2
2
 
 
3
from patches import parse_patches
3
4
import os
4
5
import sys
5
 
import subprocess
6
 
from datetime import datetime
7
 
from errors import CommandError, PatchFailed
 
6
import string
 
7
import glob
 
8
import bzrlib
 
9
from bzrlib.commands import Command
 
10
from bzrlib.branch import Branch
 
11
from bzrlib import DEFAULT_IGNORE
8
12
from hunk_selector import ShelveHunkSelector, UnshelveHunkSelector
9
 
from patchsource import PatchSource, FilePatchSource
10
 
from bzrlib.osutils import rename
 
13
from diffstat import DiffStat
 
14
 
 
15
DEFAULT_IGNORE.append('./.bzr-shelf*')
 
16
 
 
17
class QuitException(Exception):
 
18
    pass
11
19
 
12
20
class Shelf(object):
13
 
    MESSAGE_PREFIX = "# Shelved patch: "
14
 
 
15
 
    _paths = {
16
 
        'base'          : '.shelf',
17
 
        'shelves'       : '.shelf/shelves',
18
 
        'current-shelf' : '.shelf/current-shelf',
19
 
    }
20
 
 
21
 
    def __init__(self, base, name=None):
22
 
        self.base = base
23
 
        self.__setup()
24
 
 
25
 
        if name is None:
26
 
            current = os.path.join(self.base, self._paths['current-shelf'])
27
 
            name = open(current).read().strip()
28
 
 
29
 
        assert '\n' not in name
30
 
        self.name = name
31
 
 
32
 
        self.dir = os.path.join(self.base, self._paths['shelves'], name)
33
 
        if not os.path.isdir(self.dir):
34
 
            os.mkdir(self.dir)
35
 
 
36
 
    def __setup(self):
37
 
        # Create required directories etc.
38
 
        for dir in [self._paths['base'], self._paths['shelves']]:
39
 
            dir = os.path.join(self.base, dir)
40
 
            if not os.path.isdir(dir):
41
 
                os.mkdir(dir)
42
 
 
43
 
        current = os.path.join(self.base, self._paths['current-shelf'])
44
 
        if not os.path.exists(current):
45
 
            f = open(current, 'w')
46
 
            f.write('default')
47
 
            f.close()
48
 
 
49
 
    def make_default(self):
50
 
        f = open(os.path.join(self.base, self._paths['current-shelf']), 'w')
51
 
        f.write(self.name)
52
 
        f.close()
53
 
        self.log("Default shelf is now '%s'\n" % self.name)
54
 
 
55
 
    def log(self, msg):
56
 
        sys.stderr.write(msg)
57
 
 
58
 
    def delete(self, patch):
59
 
        path = self.__path_from_user(patch)
60
 
        rename(path, '%s~' % path)
61
 
 
62
 
    def display(self, patch=None):
63
 
        if patch is None:
64
 
            path = self.last_patch()
65
 
        else:
66
 
            path = self.__path_from_user(patch)
67
 
        sys.stdout.write(open(path).read())
68
 
 
69
 
    def list(self):
70
 
        indexes = self.__list()
71
 
        self.log("Patches on shelf '%s':" % self.name)
72
 
        if len(indexes) == 0:
73
 
            self.log(' None\n')
74
 
            return
75
 
        self.log('\n')
76
 
        for index in indexes:
77
 
            msg = self.get_patch_message(self.__path(index))
78
 
            if msg is None:
79
 
                msg = "No message saved with patch."
80
 
            self.log(' %.2d: %s\n' % (index, msg))
81
 
 
82
 
    def __path_from_user(self, patch_id):
83
 
        try:
84
 
            patch_index = int(patch_id)
85
 
        except (TypeError, ValueError):
86
 
            raise CommandError("Invalid patch name '%s'" % patch_id)
87
 
 
88
 
        path = self.__path(patch_index)
89
 
 
90
 
        if not os.path.exists(path):
91
 
            raise CommandError("Patch '%s' doesn't exist on shelf %s!" % \
92
 
                        (patch_id, self.name))
93
 
 
94
 
        return path
95
 
 
96
 
    def __path(self, index):
97
 
        return os.path.join(self.dir, '%.2d' % index)
98
 
 
99
 
    def next_patch(self):
100
 
        indexes = self.__list()
101
 
 
102
 
        if len(indexes) == 0:
103
 
            next = 0
104
 
        else:
105
 
            next = indexes[-1] + 1
106
 
        return self.__path(next)
107
 
 
108
 
    def __list(self):
109
 
        patches = os.listdir(self.dir)
110
 
        indexes = []
111
 
        for f in patches:
112
 
            if f.endswith('~'):
113
 
                continue # ignore backup files
114
 
            try:
115
 
                indexes.append(int(f))
116
 
            except ValueError:
117
 
                self.log("Warning: Ignoring junk file '%s' on shelf.\n" % f)
118
 
 
 
21
    def __init__(self, location, name='default'):
 
22
        self.branch = Branch.open_containing(location)[0]
 
23
        base = self.branch.controlfilename('x-shelf')
 
24
        self.shelf_dir = os.path.join(base, name)
 
25
 
 
26
        # FIXME surely there's an easier way to do this?
 
27
        t = self.branch._transport
 
28
        for dir in [base, self.shelf_dir]:
 
29
            if not t.has(dir):
 
30
                t.mkdir(dir)
 
31
 
 
32
    def __path(self, idx):
 
33
        return os.path.join(self.shelf_dir, '%.2d' % idx)
 
34
 
 
35
    def next_shelf(self):
 
36
        index = 0
 
37
        while True:
 
38
            name = self.__path(index)
 
39
            if not os.path.exists(name):
 
40
                return name
 
41
            index += 1
 
42
 
 
43
    def last_shelf(self):
 
44
        shelves = os.listdir(self.shelf_dir)
 
45
        indexes = [int(f) for f in shelves]
119
46
        indexes.sort()
120
 
        return indexes
121
 
 
122
 
    def last_patch(self):
123
 
        indexes = self.__list()
124
47
 
125
48
        if len(indexes) == 0:
126
49
            return None
127
50
 
128
51
        return self.__path(indexes[-1])
129
52
 
130
 
    def get_patch_message(self, patch_path):
131
 
        patch = open(patch_path, 'r').read()
132
 
 
133
 
        if not patch.startswith(self.MESSAGE_PREFIX):
 
53
    def get_shelf_message(self, shelf):
 
54
        prefix = "# shelf: "
 
55
        if not shelf.startswith(prefix):
134
56
            return None
135
 
        return patch[len(self.MESSAGE_PREFIX):patch.index('\n')]
136
 
 
137
 
    def unshelve(self, patch_source, patch_name=None, all=False, force=False):
138
 
        self._check_upgrade()
139
 
 
140
 
        if patch_name is None:
141
 
            patch_path = self.last_patch()
 
57
        return shelf[len(prefix):shelf.index('\n')]
 
58
 
 
59
    def unshelve(self, pick_hunks=False):
 
60
        shelf = self.last_shelf()
 
61
 
 
62
        if shelf is None:
 
63
            raise Exception("No shelf found in '%s'" % self.branch.base)
 
64
 
 
65
        patches = parse_patches(open(shelf, 'r').readlines())
 
66
        if pick_hunks:
 
67
            try:
 
68
                to_unshelve, to_remain = UnshelveHunkSelector(patches).select()
 
69
            except QuitException:
 
70
                return False
142
71
        else:
143
 
            patch_path = self.__path_from_user(patch_name)
144
 
 
145
 
        if patch_path is None:
146
 
            raise CommandError("No patch found on shelf %s" % self.name)
147
 
 
148
 
        patches = FilePatchSource(patch_path).readpatches()
149
 
        if all:
150
72
            to_unshelve = patches
151
73
            to_remain = []
152
 
        else:
153
 
            to_unshelve, to_remain = UnshelveHunkSelector(patches).select()
154
74
 
155
75
        if len(to_unshelve) == 0:
156
 
            raise CommandError('Nothing to unshelve')
157
 
 
158
 
        message = self.get_patch_message(patch_path)
159
 
        if message is None:
160
 
            message = "No message saved with patch."
161
 
        self.log('Unshelving from %s/%s: "%s"\n' % \
162
 
                (self.name, os.path.basename(patch_path), message))
163
 
 
164
 
        try:
165
 
            self._run_patch(to_unshelve, dry_run=True)
166
 
            self._run_patch(to_unshelve)
167
 
        except PatchFailed:
168
 
            try:
169
 
                self._run_patch(to_unshelve, strip=1, dry_run=True)
170
 
                self._run_patch(to_unshelve, strip=1)
171
 
            except PatchFailed:
172
 
                if force:
173
 
                    self.log('Warning: Unshelving failed, forcing as ' \
174
 
                             'requested. Shelf will not be modified.\n')
175
 
                    try:
176
 
                        self._run_patch(to_unshelve)
177
 
                    except PatchFailed:
178
 
                        pass
179
 
                    return
180
 
                raise CommandError("Your shelved patch no " \
181
 
                    "longer applies cleanly to the working tree!")
182
 
 
183
 
        # Backup the shelved patch
184
 
        rename(patch_path, '%s~' % patch_path)
185
 
 
186
 
        if len(to_remain) > 0:
187
 
            f = open(patch_path, 'w')
 
76
            print >>sys.stderr, 'Nothing to unshelve'
 
77
            return True
 
78
 
 
79
        print >>sys.stderr, "Reapplying shelved patches",
 
80
        message = self.get_shelf_message(open(shelf, 'r').read())
 
81
        if message is not None:
 
82
            print >>sys.stderr, ' "%s"' % message
 
83
        else:
 
84
            print >>sys.stderr, ""
 
85
 
 
86
        pipe = os.popen('patch -d %s -s -p0' % self.branch.base, 'w')
 
87
        for patch in to_unshelve:
 
88
            pipe.write(str(patch))
 
89
        pipe.flush()
 
90
 
 
91
        if pipe.close() is not None:
 
92
            raise Exception("Failed running patch!")
 
93
 
 
94
        if len(to_remain) == 0:
 
95
            os.remove(shelf)
 
96
        else:
 
97
            f = open(shelf, 'w')
188
98
            for patch in to_remain:
189
99
                f.write(str(patch))
190
100
            f.close()
191
101
 
192
 
    def shelve(self, patch_source, all=False, message=None):
193
 
        self._check_upgrade()
194
 
 
195
 
        patches = patch_source.readpatches()
196
 
 
197
 
        if all:
 
102
        diff_stat = DiffStat(self.get_patches(None, None))
 
103
        print 'Diff status is now:\n', diff_stat
 
104
 
 
105
        return True
 
106
 
 
107
    def get_patches(self, revision, file_list):
 
108
        from StringIO import StringIO
 
109
        from bzrlib.diff import show_diff
 
110
        out = StringIO()
 
111
        show_diff(self.branch, revision, specific_files=file_list, output=out)
 
112
        out.seek(0)
 
113
        return out.readlines()
 
114
 
 
115
    def shelve(self, pick_hunks=False, message=None, revision=None,
 
116
             file_list=None):
 
117
        patches = parse_patches(self.get_patches(revision, file_list))
 
118
 
 
119
        if pick_hunks:
 
120
            try:
 
121
                to_shelve = ShelveHunkSelector(patches).select()[0]
 
122
            except QuitException:
 
123
                return False
 
124
        else:
198
125
            to_shelve = patches
199
 
        else:
200
 
            to_shelve = ShelveHunkSelector(patches).select()[0]
201
126
 
202
127
        if len(to_shelve) == 0:
203
 
            raise CommandError('Nothing to shelve')
204
 
 
205
 
        if message is None:
206
 
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
207
 
            message = "Changes shelved on %s" % timestamp
208
 
 
209
 
        patch_path = self.next_patch()
210
 
        self.log('Shelving to %s/%s: "%s"\n' % \
211
 
                (self.name, os.path.basename(patch_path), message))
212
 
 
213
 
        f = open(patch_path, 'a')
214
 
 
215
 
        assert '\n' not in message
216
 
        f.write("%s%s\n" % (self.MESSAGE_PREFIX, message))
217
 
 
218
 
        for patch in to_shelve:
219
 
            f.write(str(patch))
220
 
 
221
 
        f.flush()
222
 
        os.fsync(f.fileno())
223
 
        f.close()
224
 
 
225
 
        try:
226
 
            self._run_patch(to_shelve, reverse=True, dry_run=True)
227
 
            self._run_patch(to_shelve, reverse=True)
228
 
        except PatchFailed:
229
 
            try:
230
 
                self._run_patch(to_shelve, reverse=True, strip=1, dry_run=True)
231
 
                self._run_patch(to_shelve, reverse=True, strip=1)
232
 
            except PatchFailed:
233
 
                raise CommandError("Failed removing shelved changes from the"
234
 
                    "working tree!")
235
 
 
236
 
    def _run_patch(self, patches, strip=0, reverse=False, dry_run=False):
237
 
        args = ['patch', '-d', self.base, '-s', '-p%d' % strip, '-f']
238
 
 
239
 
        if sys.platform == "win32":
240
 
            args.append('--binary')
241
 
 
242
 
        if reverse:
243
 
            args.append('-R')
244
 
        if dry_run:
245
 
            args.append('--dry-run')
246
 
            stdout = stderr = subprocess.PIPE
247
 
        else:
248
 
            stdout = stderr = None
249
 
 
250
 
        process = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=stdout,
251
 
                        stderr=stderr)
252
 
        for patch in patches:
253
 
            process.stdin.write(str(patch))
254
 
 
255
 
        process.communicate()
256
 
 
257
 
        result = process.wait()
258
 
        if result != 0:
259
 
            raise PatchFailed()
260
 
 
261
 
        return result
262
 
 
263
 
    def _check_upgrade(self):
264
 
        if len(self._list_old_shelves()) > 0:
265
 
            raise CommandError("Old format shelves found, either upgrade " \
266
 
                    "or remove them!")
267
 
 
268
 
    def _list_old_shelves(self):
269
 
        import glob
270
 
        stem = os.path.join(self.base, '.bzr-shelf')
271
 
 
272
 
        patches = glob.glob(stem)
273
 
        patches.extend(glob.glob(stem + '-*[!~]'))
274
 
 
275
 
        if len(patches) == 0:
276
 
            return []
277
 
 
278
 
        def patch_index(name):
279
 
            if name == stem:
280
 
                return 0
281
 
            return int(name[len(stem) + 1:])
282
 
 
283
 
        # patches might not be sorted in the right order
284
 
        patch_ids = []
285
 
        for patch in patches:
286
 
            if patch == stem:
287
 
                patch_ids.append(0)
288
 
            else:
289
 
                patch_ids.append(int(patch[len(stem) + 1:]))
290
 
 
291
 
        patch_ids.sort()
292
 
 
293
 
        patches = []
294
 
        for id in patch_ids:
295
 
            if id == 0:
296
 
                patches.append(stem)
297
 
            else:
298
 
                patches.append('%s-%s' % (stem, id))
299
 
 
300
 
        return patches
301
 
 
302
 
    def upgrade(self):
303
 
        patches = self._list_old_shelves()
304
 
 
305
 
        if len(patches) == 0:
306
 
            self.log('No old-style shelves found to upgrade.\n')
307
 
            return
308
 
 
309
 
        for patch in patches:
310
 
            old_file = open(patch, 'r')
311
 
            new_path = self.next_patch()
312
 
            new_file = open(new_path, 'w')
313
 
            new_file.write(old_file.read())
314
 
            old_file.close()
315
 
            new_file.close()
316
 
            self.log('Copied %s to %s/%s\n' % (os.path.basename(patch),
317
 
                self.name, os.path.basename(new_path)))
318
 
            rename(patch, patch + '~')
 
128
            print >>sys.stderr, 'Nothing to shelve'
 
129
            return True
 
130
 
 
131
        shelf = self.next_shelf()
 
132
        print >>sys.stderr, "Saving shelved patches to", shelf
 
133
        shelf = open(shelf, 'a')
 
134
        if message is not None:
 
135
            assert '\n' not in message
 
136
            shelf.write("# shelf: %s\n" % message)
 
137
        for patch in to_shelve:
 
138
            shelf.write(str(patch))
 
139
 
 
140
        shelf.flush()
 
141
        os.fsync(shelf.fileno())
 
142
        shelf.close()
 
143
 
 
144
        print >>sys.stderr, "Reverting shelved patches"
 
145
        pipe = os.popen('patch -d %s -sR -p0' % self.branch.base, 'w')
 
146
        for patch in to_shelve:
 
147
            pipe.write(str(patch))
 
148
        pipe.flush()
 
149
 
 
150
        if pipe.close() is not None:
 
151
            raise Exception("Failed running patch!")
 
152
 
 
153
        diff_stat = DiffStat(self.get_patches(None, None))
 
154
        print 'Diff status is now:\n', diff_stat
 
155
 
 
156
        return True
 
157