~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.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
 
# Copyright (C) 2005 Aaron Bentley
2
 
# <aaron.bentley@utoronto.ca>
3
 
#
4
 
#    This program is free software; you can redistribute it and/or modify
5
 
#    it under the terms of the GNU General Public License as published by
6
 
#    the Free Software Foundation; either version 2 of the License, or
7
 
#    (at your option) any later version.
8
 
#
9
 
#    This program is distributed in the hope that it will be useful,
10
 
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 
#    GNU General Public License for more details.
13
 
#
14
 
#    You should have received a copy of the GNU General Public License
15
 
#    along with this program; if not, write to the Free Software
16
 
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
 
import codecs
18
 
import errno
19
 
import os
20
 
import re
21
 
import tempfile
22
 
import shutil
23
 
from subprocess import Popen, PIPE
24
 
import sys
25
 
 
26
 
import bzrlib
27
 
import bzrlib.errors
28
 
from bzrlib.errors import (BzrCommandError, NotBranchError, NoSuchFile,
29
 
                           UnsupportedFormatError, TransportError, 
30
 
                           NoWorkingTree, PermissionDenied)
31
 
from bzrlib.bzrdir import BzrDir, BzrDirFormat
32
 
 
33
 
def temp_tree():
34
 
    dirname = tempfile.mkdtemp("temp-branch")
35
 
    return BzrDir.create_standalone_workingtree(dirname)
36
 
 
37
 
def rm_tree(tree):
38
 
    shutil.rmtree(tree.basedir)
39
 
 
40
 
def is_clean(cur_tree):
41
 
    """
42
 
    Return true if no files are modifed or unknown
43
 
    >>> import bzrlib.add
44
 
    >>> tree = temp_tree()
45
 
    >>> is_clean(tree)
46
 
    (True, [])
47
 
    >>> fooname = os.path.join(tree.basedir, "foo")
48
 
    >>> file(fooname, "wb").write("bar")
49
 
    >>> is_clean(tree)
50
 
    (True, [u'foo'])
51
 
    >>> bzrlib.add.smart_add_tree(tree, [tree.basedir])
52
 
    ([u'foo'], {})
53
 
    >>> is_clean(tree)
54
 
    (False, [])
55
 
    >>> tree.commit("added file", rev_id='commit-id')
56
 
    'commit-id'
57
 
    >>> is_clean(tree)
58
 
    (True, [])
59
 
    >>> rm_tree(tree)
60
 
    """
61
 
    from bzrlib.diff import compare_trees
62
 
    old_tree = cur_tree.basis_tree()
63
 
    new_tree = cur_tree
64
 
    non_source = []
65
 
    for path, file_class, kind, file_id, entry in new_tree.list_files():
66
 
        if file_class in ('?', 'I'):
67
 
            non_source.append(path)
68
 
    delta = compare_trees(old_tree, new_tree, want_unchanged=False)
69
 
    return not delta.has_changed(), non_source
70
 
 
71
 
def set_push_data(tree, location):
72
 
    tree.branch.control_files.put_utf8("x-push-data", "%s\n" % location)
73
 
 
74
 
def get_push_data(tree):
75
 
    """
76
 
    >>> tree = temp_tree()
77
 
    >>> get_push_data(tree) is None
78
 
    True
79
 
    >>> set_push_data(tree, 'http://somewhere')
80
 
    >>> get_push_data(tree)
81
 
    u'http://somewhere'
82
 
    >>> rm_tree(tree)
83
 
    """
84
 
    try:
85
 
        location = tree.branch.control_files.get_utf8('x-push-data').read()
86
 
    except NoSuchFile:
87
 
        return None
88
 
    return location.rstrip('\n')
89
 
 
90
 
"""
91
 
>>> shell_escape('hello')
92
 
'\h\e\l\l\o'
93
 
"""
94
 
def shell_escape(arg):
95
 
    return "".join(['\\'+c for c in arg])
96
 
 
97
 
def safe_system(args):
98
 
    """
99
 
    >>> real_system = os.system
100
 
    >>> os.system = sys.stdout.write
101
 
    >>> safe_system(['a', 'b', 'cd'])
102
 
    \\a \\b \\c\\d
103
 
    >>> os.system = real_system
104
 
    """
105
 
    arg_str = " ".join([shell_escape(a) for a in args])
106
 
    return os.system(arg_str)
107
 
 
108
 
class RsyncUnknownStatus(Exception):
109
 
    def __init__(self, status):
110
 
        Exception.__init__(self, "Unknown status: %d" % status)
111
 
 
112
 
class NoRsync(Exception):
113
 
    def __init__(self, rsync_name):
114
 
        Exception.__init__(self, "%s not found." % rsync_name)
115
 
 
116
 
def rsync(source, target, ssh=False, excludes=(), silent=False, 
117
 
          rsync_name="rsync"):
118
 
    """
119
 
    >>> new_dir = tempfile.mkdtemp()
120
 
    >>> old_dir = os.getcwd()
121
 
    >>> os.chdir(new_dir)
122
 
    >>> rsync("a", "b", silent=True)
123
 
    Traceback (most recent call last):
124
 
    RsyncNoFile: No such file...
125
 
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True)
126
 
    Traceback (most recent call last):
127
 
    RsyncNoFile: No such file...
128
 
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
129
 
    Traceback (most recent call last):
130
 
    NoRsync: rsyncc not found.
131
 
    >>> os.chdir(old_dir)
132
 
    >>> os.rmdir(new_dir)
133
 
    """
134
 
    cmd = [rsync_name, "-av", "--delete"]
135
 
    if ssh:
136
 
        cmd.extend(('-e', 'ssh'))
137
 
    if len(excludes) > 0:
138
 
        cmd.extend(('--exclude-from', '-'))
139
 
    cmd.extend((source, target))
140
 
    if silent:
141
 
        stderr = PIPE
142
 
        stdout = PIPE
143
 
    else:
144
 
        stderr = None
145
 
        stdout = None
146
 
    try:
147
 
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
148
 
    except OSError, e:
149
 
        if e.errno == errno.ENOENT:
150
 
            raise NoRsync(rsync_name)
151
 
            
152
 
    proc.stdin.write('\n'.join(excludes)+'\n')
153
 
    proc.stdin.close()
154
 
    if silent:
155
 
        proc.stderr.read()
156
 
        proc.stderr.close()
157
 
        proc.stdout.read()
158
 
        proc.stdout.close()
159
 
    proc.wait()
160
 
    if proc.returncode == 12:
161
 
        raise RsyncStreamIO()
162
 
    elif proc.returncode == 23:
163
 
        raise RsyncNoFile(source)
164
 
    elif proc.returncode != 0:
165
 
        raise RsyncUnknownStatus(proc.returncode)
166
 
    return cmd
167
 
 
168
 
 
169
 
def rsync_ls(source, ssh=False, silent=True):
170
 
    cmd = ["rsync"]
171
 
    if ssh:
172
 
        cmd.extend(('-e', 'ssh'))
173
 
    cmd.append(source)
174
 
    if silent:
175
 
        stderr = PIPE
176
 
    else:
177
 
        stderr = None
178
 
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
179
 
    result = proc.stdout.read()
180
 
    proc.stdout.close()
181
 
    if silent:
182
 
        proc.stderr.read()
183
 
        proc.stderr.close()
184
 
    proc.wait()
185
 
    if proc.returncode == 12:
186
 
        raise RsyncStreamIO()
187
 
    elif proc.returncode == 23:
188
 
        raise RsyncNoFile(source)
189
 
    elif proc.returncode != 0:
190
 
        raise RsyncUnknownStatus(proc.returncode)
191
 
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
192
 
 
193
 
exclusions = ('.bzr/x-push-data', '.bzr/branch/x-push/data', '.bzr/parent', 
194
 
              '.bzr/branch/parent', '.bzr/x-pull-data', '.bzr/x-pull',
195
 
              '.bzr/pull', '.bzr/stat-cache', '.bzr/x-rsync-data',
196
 
              '.bzr/basis-inventory', '.bzr/inventory.backup.weave')
197
 
 
198
 
 
199
 
def read_revision_history(fname):
200
 
    return [l.rstrip('\r\n') for l in
201
 
            codecs.open(fname, 'rb', 'utf-8').readlines()]
202
 
 
203
 
class RsyncNoFile(Exception):
204
 
    def __init__(self, path):
205
 
        Exception.__init__(self, "No such file %s" % path)
206
 
 
207
 
class RsyncStreamIO(Exception):
208
 
    def __init__(self):
209
 
        Exception.__init__(self, "Error in rsync protocol data stream.")
210
 
 
211
 
def get_revision_history(location):
212
 
    tempdir = tempfile.mkdtemp('push')
213
 
    try:
214
 
        history_fname = os.path.join(tempdir, 'revision-history')
215
 
        try:
216
 
            cmd = rsync(location+'.bzr/revision-history', history_fname,
217
 
                        silent=True)
218
 
        except RsyncNoFile:
219
 
            cmd = rsync(location+'.bzr/branch/revision-history', history_fname,
220
 
                        silent=True)
221
 
        history = read_revision_history(history_fname)
222
 
    finally:
223
 
        shutil.rmtree(tempdir)
224
 
    return history
225
 
 
226
 
def history_subset(location, branch):
227
 
    remote_history = get_revision_history(location)
228
 
    local_history = branch.revision_history()
229
 
    if len(remote_history) > len(local_history):
230
 
        return False
231
 
    for local, remote in zip(remote_history, local_history):
232
 
        if local != remote:
233
 
            return False 
234
 
    return True
235
 
 
236
 
def empty_or_absent(location):
237
 
    try:
238
 
        files = rsync_ls(location)
239
 
        return files == ['.']
240
 
    except RsyncNoFile:
241
 
        return True
242
 
 
243
 
def rspush(tree, location=None, overwrite=False, working_tree=True):
244
 
    push_location = get_push_data(tree)
245
 
    if location is not None:
246
 
        if not location.endswith('/'):
247
 
            location += '/'
248
 
        push_location = location
249
 
    
250
 
    if push_location is None:
251
 
        raise BzrCommandError("No rspush location known or specified.")
252
 
 
253
 
    if (push_location.find('://') != -1 or
254
 
        push_location.find(':') == -1):
255
 
        raise BzrCommandError("Invalid rsync path %r." % push_location)
256
 
 
257
 
    if working_tree:
258
 
        clean, non_source = is_clean(tree)
259
 
        if not clean:
260
 
            print """Error: This tree has uncommitted changes or unknown (?) files.
261
 
    Use "bzr status" to list them."""
262
 
            sys.exit(1)
263
 
        final_exclusions = non_source[:]
264
 
    else:
265
 
        wt = tree
266
 
        final_exclusions = []
267
 
        for path, status, kind, file_id, entry in wt.list_files():
268
 
            final_exclusions.append(path)
269
 
 
270
 
    final_exclusions.extend(exclusions)
271
 
    if not overwrite:
272
 
        try:
273
 
            if not history_subset(push_location, tree.branch):
274
 
                raise bzrlib.errors.BzrCommandError("Local branch is not a"
275
 
                                                    " newer version of remote"
276
 
                                                    " branch.")
277
 
        except RsyncNoFile:
278
 
            if not empty_or_absent(push_location):
279
 
                raise bzrlib.errors.BzrCommandError("Remote location is not a"
280
 
                                                    " bzr branch (or empty"
281
 
                                                    " directory)")
282
 
        except RsyncStreamIO:
283
 
            raise bzrlib.errors.BzrCommandError("Rsync could not use the"
284
 
                " specified location.  Please ensure that"
285
 
                ' "%s" is of the form "machine:/path".' % push_location)
286
 
    print "Pushing to %s" % push_location
287
 
    rsync(tree.basedir+'/', push_location, ssh=True, 
288
 
          excludes=final_exclusions)
289
 
 
290
 
    set_push_data(tree, push_location)
291
 
 
292
 
 
293
 
def short_committer(committer):
294
 
    new_committer = re.sub('<.*>', '', committer).strip(' ')
295
 
    if len(new_committer) < 2:
296
 
        return committer
297
 
    return new_committer
298
 
 
299
 
 
300
 
def apache_ls(t):
301
 
    """Screen-scrape Apache listings"""
302
 
    apache_dir = '<img border="0" src="/icons/folder.gif" alt="[dir]">'\
303
 
        ' <a href="'
304
 
    lines = t.get('.')
305
 
    expr = re.compile('<a[^>]*href="([^>]*)"[^>]*>', flags=re.I)
306
 
    for line in lines:
307
 
        match = expr.search(line)
308
 
        if match is None:
309
 
            continue
310
 
        url = match.group(1)
311
 
        if url.startswith('http://') or url.startswith('/') or '../' in url:
312
 
            continue
313
 
        if '?' in url:
314
 
            continue
315
 
        yield url.rstrip('/')
316
 
 
317
 
 
318
 
def iter_branches(t, lister=None):
319
 
    """Iterate through all the branches under a transport"""
320
 
    for bzrdir in iter_bzrdirs(t, lister):
321
 
        try:
322
 
            branch = bzrdir.open_branch()
323
 
            if branch.bzrdir is bzrdir:
324
 
                yield branch
325
 
        except (NotBranchError, UnsupportedFormatError):
326
 
            pass
327
 
 
328
 
 
329
 
def iter_branch_tree(t, lister=None):
330
 
    for bzrdir in iter_bzrdirs(t, lister):
331
 
        try:
332
 
            wt = bzrdir.open_workingtree()
333
 
            yield wt.branch, wt
334
 
        except NoWorkingTree, UnsupportedFormatError:
335
 
            try:
336
 
                branch = bzrdir.open_branch()
337
 
                if branch.bzrdir is bzrdir:
338
 
                    yield branch, None
339
 
            except (NotBranchError, UnsupportedFormatError):
340
 
                continue
341
 
 
342
 
 
343
 
def iter_bzrdirs(t, lister=None):
344
 
    if lister is None:
345
 
        def lister(t):
346
 
            return t.list_dir('.')
347
 
    try:
348
 
        bzrdir = bzrdir_from_transport(t)
349
 
        yield bzrdir
350
 
    except (NotBranchError, UnsupportedFormatError, TransportError,
351
 
            PermissionDenied):
352
 
        pass
353
 
    try:
354
 
        for directory in lister(t):
355
 
            if directory == ".bzr":
356
 
                continue
357
 
            try:
358
 
                subt = t.clone(directory)
359
 
            except UnicodeDecodeError:
360
 
                continue
361
 
            for bzrdir in iter_bzrdirs(subt, lister):
362
 
                yield bzrdir
363
 
    except (NoSuchFile, PermissionDenied, TransportError):
364
 
        pass
365
 
 
366
 
    
367
 
def bzrdir_from_transport(t):
368
 
    """Open a bzrdir from a transport (not a location)"""
369
 
    format = BzrDirFormat.find_format(t)
370
 
    BzrDir._check_supported(format, False)
371
 
    return format.open(t)
372
 
 
373
 
 
374
 
def run_tests():
375
 
    import doctest
376
 
    result = doctest.testmod()
377
 
    if result[1] > 0:
378
 
        if result[0] == 0:
379
 
            print "All tests passed"
380
 
    else:
381
 
        print "No tests to run"
382
 
if __name__ == "__main__":
383
 
    run_tests()