]> arthur.barton.de Git - bup.git/blob - lib/bup/cmd/get.py
Convert bup to binary executable and run python subcommands directly
[bup.git] / lib / bup / cmd / get.py
1
2 from __future__ import absolute_import, print_function
3 from binascii import hexlify, unhexlify
4 from collections import namedtuple
5 from functools import partial
6 from stat import S_ISDIR
7 import os, sys, textwrap, time
8
9 from bup import compat, git, client, helpers, vfs
10 from bup.compat import (
11     argv_bytes,
12     bytes_from_byte,
13     environ,
14     hexstr,
15     items,
16     wrap_main
17 )
18 from bup.git import get_cat_data, parse_commit, walk_object
19 from bup.helpers import add_error, debug1, handle_ctrl_c, log, saved_errors
20 from bup.helpers import hostname, shstr, tty_width
21 from bup.io import path_msg
22 from bup.pwdgrp import userfullname, username
23 from bup.repo import LocalRepo, RemoteRepo
24
25 argspec = (
26     "usage: bup get [-s source] [-r remote] (<--ff|--append|...> REF [DEST])...",
27
28     """Transfer data from a source repository to a destination repository
29     according to the methods specified (--ff, --ff:, --append, etc.).
30     Both repositories default to BUP_DIR.  A remote destination may be
31     specified with -r, and data may be pulled from a remote repository
32     with the related "bup on HOST get ..." command.""",
33
34     ('optional arguments:',
35      (('-h, --help', 'show this help message and exit'),
36       ('-v, --verbose',
37        'increase log output (can be specified more than once)'),
38       ('-q, --quiet', "don't show progress meter"),
39       ('-s SOURCE, --source SOURCE',
40        'path to the source repository (defaults to BUP_DIR)'),
41       ('-r REMOTE, --remote REMOTE',
42        'hostname:/path/to/repo of remote destination repository'),
43       ('-t --print-trees', 'output a tree id for each ref set'),
44       ('-c, --print-commits', 'output a commit id for each ref set'),
45       ('--print-tags', 'output an id for each tag'),
46       ('--bwlimit BWLIMIT', 'maximum bytes/sec to transmit to server'),
47       ('-0, -1, -2, -3, -4, -5, -6, -7, -8, -9, --compress LEVEL',
48        'set compression LEVEL (default: 1)'))),
49
50     ('transfer methods:',
51      (('--ff REF, --ff: REF DEST',
52        'fast-forward dest REF (or DEST) to match source REF'),
53       ('--append REF, --append: REF DEST',
54        'append REF (treeish or committish) to dest REF (or DEST)'),
55       ('--pick REF, --pick: REF DEST',
56        'append single source REF commit to dest REF (or DEST)'),
57       ('--force-pick REF, --force-pick: REF DEST',
58        '--pick, overwriting REF (or DEST)'),
59       ('--new-tag REF, --new-tag: REF DEST',
60        'tag source ref REF as REF (or DEST) in dest unless it already exists'),
61       ('--replace, --replace: REF DEST',
62        'overwrite REF (or DEST) in dest with source REF'),
63       ('--unnamed REF',
64        'fetch REF anonymously (without destination ref)'))))
65
66 def render_opts(opts, width=None):
67     if not width:
68         width = tty_width()
69     result = []
70     for args, desc in opts:
71         result.append(textwrap.fill(args, width=width,
72                                     initial_indent=(' ' * 2),
73                                     subsequent_indent=(' ' * 4)))
74         result.append('\n')
75         result.append(textwrap.fill(desc, width=width,
76                                     initial_indent=(' ' * 6),
77                                     subsequent_indent=(' ' * 6)))
78         result.append('\n')
79     return result
80
81 def usage(argspec, width=None):
82     if not width:
83         width = tty_width()
84     usage, preamble, groups = argspec[0], argspec[1], argspec[2:]
85     msg = []
86     msg.append(textwrap.fill(usage, width=width, subsequent_indent='  '))
87     msg.append('\n\n')
88     msg.append(textwrap.fill(preamble.replace('\n', ' '), width=width))
89     msg.append('\n')
90     for group_name, group_args in groups:
91         msg.extend(['\n', group_name, '\n'])
92         msg.extend(render_opts(group_args, width=width))
93     return ''.join(msg)
94
95 def misuse(message=None):
96     sys.stderr.write(usage(argspec))
97     if message:
98         sys.stderr.write('\nerror: ')
99         sys.stderr.write(message)
100         sys.stderr.write('\n')
101     sys.exit(1)
102
103 def require_n_args_or_die(n, args):
104     if len(args) < n + 1:
105         misuse('%s argument requires %d %s'
106                % (n, 'values' if n == 1 else 'value'))
107     result = args[1:1+n], args[1+n:]
108     assert len(result[0]) == n
109     return result
110
111 Spec = namedtuple('Spec', ('method', 'src', 'dest'))
112
113 def spec_msg(s):
114     if not s.dest:
115         return '--%s %s' % (s.method, path_msg(s.src))
116     return '--%s: %s %s' % (s.method, path_msg(s.src), path_msg(s.dest))
117
118 def parse_args(args):
119     class GetOpts:
120         pass
121     opt = GetOpts()
122     opt.help = False
123     opt.verbose = 0
124     opt.quiet = False
125     opt.print_commits = opt.print_trees = opt.print_tags = False
126     opt.bwlimit = None
127     opt.compress = 1
128     opt.source = opt.remote = None
129     opt.target_specs = []
130
131     remaining = args[1:]  # Skip argv[0]
132     while remaining:
133         arg = remaining[0]
134         if arg in (b'-h', b'--help'):
135             sys.stdout.write(usage(argspec))
136             sys.exit(0)
137         elif arg in (b'-v', b'--verbose'):
138             opt.verbose += 1
139             remaining = remaining[1:]
140         elif arg in (b'--ff', b'--append', b'--pick', b'--force-pick',
141                      b'--new-tag', b'--replace', b'--unnamed'):
142             (ref,), remaining = require_n_args_or_die(1, remaining)
143             opt.target_specs.append(Spec(method=arg[2:].decode('ascii'),
144                                          src=ref, dest=None))
145         elif arg in (b'--ff:', b'--append:', b'--pick:', b'--force-pick:',
146                      b'--new-tag:', b'--replace:'):
147             (ref, dest), remaining = require_n_args_or_die(2, remaining)
148             opt.target_specs.append(Spec(method=arg[2:-1].decode('ascii'),
149                                          src=ref, dest=dest))
150         elif arg in (b'-s', b'--source'):
151             (opt.source,), remaining = require_n_args_or_die(1, remaining)
152         elif arg in (b'-r', b'--remote'):
153             (opt.remote,), remaining = require_n_args_or_die(1, remaining)
154         elif arg in (b'-c', b'--print-commits'):
155             opt.print_commits, remaining = True, remaining[1:]
156         elif arg in (b'-t', b'--print-trees'):
157             opt.print_trees, remaining = True, remaining[1:]
158         elif arg == b'--print-tags':
159             opt.print_tags, remaining = True, remaining[1:]
160         elif arg in (b'-0', b'-1', b'-2', b'-3', b'-4', b'-5', b'-6', b'-7',
161                      b'-8', b'-9'):
162             opt.compress = int(arg[1:])
163             remaining = remaining[1:]
164         elif arg == b'--compress':
165             (opt.compress,), remaining = require_n_args_or_die(1, remaining)
166             opt.compress = int(opt.compress)
167         elif arg == b'--bwlimit':
168             (opt.bwlimit,), remaining = require_n_args_or_die(1, remaining)
169             opt.bwlimit = long(opt.bwlimit)
170         elif arg.startswith(b'-') and len(arg) > 2 and arg[1] != b'-':
171             # Try to interpret this as -xyz, i.e. "-xyz -> -x -y -z".
172             # We do this last so that --foo -bar is valid if --foo
173             # requires a value.
174             remaining[0:1] = (b'-' + bytes_from_byte(c) for c in arg[1:])
175             # FIXME
176             continue
177         else:
178             misuse()
179     return opt
180
181 # FIXME: client error handling (remote exceptions, etc.)
182
183 # FIXME: walk_object in in git.py doesn't support opt.verbose.  Do we
184 # need to adjust for that here?
185 def get_random_item(name, hash, repo, writer, opt):
186     def already_seen(oid):
187         return writer.exists(unhexlify(oid))
188     for item in walk_object(repo.cat, hash, stop_at=already_seen,
189                             include_data=True):
190         # already_seen ensures that writer.exists(id) is false.
191         # Otherwise, just_write() would fail.
192         writer.just_write(item.oid, item.type, item.data)
193
194
195 def append_commit(name, hash, parent, src_repo, writer, opt):
196     now = time.time()
197     items = parse_commit(get_cat_data(src_repo.cat(hash), b'commit'))
198     tree = unhexlify(items.tree)
199     author = b'%s <%s>' % (items.author_name, items.author_mail)
200     author_time = (items.author_sec, items.author_offset)
201     committer = b'%s <%s@%s>' % (userfullname(), username(), hostname())
202     get_random_item(name, hexlify(tree), src_repo, writer, opt)
203     c = writer.new_commit(tree, parent,
204                           author, items.author_sec, items.author_offset,
205                           committer, now, None,
206                           items.message)
207     return c, tree
208
209
210 def append_commits(commits, src_name, dest_hash, src_repo, writer, opt):
211     last_c, tree = dest_hash, None
212     for commit in commits:
213         last_c, tree = append_commit(src_name, commit, last_c,
214                                      src_repo, writer, opt)
215     assert(tree is not None)
216     return last_c, tree
217
218 Loc = namedtuple('Loc', ['type', 'hash', 'path'])
219 default_loc = Loc(None, None, None)
220
221 def find_vfs_item(name, repo):
222     res = repo.resolve(name, follow=False, want_meta=False)
223     leaf_name, leaf_item = res[-1]
224     if not leaf_item:
225         return None
226     kind = type(leaf_item)
227     if kind == vfs.Root:
228         kind = 'root'
229     elif kind == vfs.Tags:
230         kind = 'tags'
231     elif kind == vfs.RevList:
232         kind = 'branch'
233     elif kind == vfs.Commit:
234         if len(res) > 1 and type(res[-2][1]) == vfs.RevList:
235             kind = 'save'
236         else:
237             kind = 'commit'
238     elif kind == vfs.Item:
239         if S_ISDIR(vfs.item_mode(leaf_item)):
240             kind = 'tree'
241         else:
242             kind = 'blob'
243     elif kind == vfs.Chunky:
244         kind = 'tree'
245     elif kind == vfs.FakeLink:
246         # Don't have to worry about ELOOP, excepting malicious
247         # remotes, since "latest" is the only FakeLink.
248         assert leaf_name == b'latest'
249         res = repo.resolve(leaf_item.target, parent=res[:-1],
250                            follow=False, want_meta=False)
251         leaf_name, leaf_item = res[-1]
252         assert leaf_item
253         assert type(leaf_item) == vfs.Commit
254         name = b'/'.join(x[0] for x in res)
255         kind = 'save'
256     else:
257         raise Exception('unexpected resolution for %s: %r'
258                         % (path_msg(name), res))
259     path = b'/'.join(name for name, item in res)
260     if hasattr(leaf_item, 'coid'):
261         result = Loc(type=kind, hash=leaf_item.coid, path=path)
262     elif hasattr(leaf_item, 'oid'):
263         result = Loc(type=kind, hash=leaf_item.oid, path=path)
264     else:
265         result = Loc(type=kind, hash=None, path=path)
266     return result
267
268
269 Target = namedtuple('Target', ['spec', 'src', 'dest'])
270
271 def loc_desc(loc):
272     if loc and loc.hash:
273         loc = loc._replace(hash=hexlify(loc.hash))
274     return repr(loc)
275
276
277 # FIXME: see if resolve() means we can drop the vfs path cleanup
278
279 def cleanup_vfs_path(p):
280     result = os.path.normpath(p)
281     if result.startswith(b'/'):
282         return result
283     return b'/' + result
284
285
286 def validate_vfs_path(p):
287     if p.startswith(b'/.') \
288        and not p.startswith(b'/.tag/'):
289         misuse('unsupported destination path %s in %s'
290                % (path_msg(dest.path), spec_msg(spec)))
291     return p
292
293
294 def resolve_src(spec, src_repo):
295     src = find_vfs_item(spec.src, src_repo)
296     spec_args = spec_msg(spec)
297     if not src:
298         misuse('cannot find source for %s' % spec_args)
299     if src.type == 'root':
300         misuse('cannot fetch entire repository for %s' % spec_args)
301     if src.type == 'tags':
302         misuse('cannot fetch entire /.tag directory for %s' % spec_args)
303     debug1('src: %s\n' % loc_desc(src))
304     return src
305
306
307 def get_save_branch(repo, path):
308     res = repo.resolve(path, follow=False, want_meta=False)
309     leaf_name, leaf_item = res[-1]
310     if not leaf_item:
311         misuse('error: cannot access %r in %r' % (leaf_name, path))
312     assert len(res) == 3
313     res_path = b'/'.join(name for name, item in res[:-1])
314     return res_path
315
316
317 def resolve_branch_dest(spec, src, src_repo, dest_repo):
318     # Resulting dest must be treeish, or not exist.
319     if not spec.dest:
320         # Pick a default dest.
321         if src.type == 'branch':
322             spec = spec._replace(dest=spec.src)
323         elif src.type == 'save':
324             spec = spec._replace(dest=get_save_branch(src_repo, spec.src))
325         elif src.path.startswith(b'/.tag/'):  # Dest defaults to the same.
326             spec = spec._replace(dest=spec.src)
327
328     spec_args = spec_msg(spec)
329     if not spec.dest:
330         misuse('no destination (implicit or explicit) for %s', spec_args)
331
332     dest = find_vfs_item(spec.dest, dest_repo)
333     if dest:
334         if dest.type == 'commit':
335             misuse('destination for %s is a tagged commit, not a branch'
336                   % spec_args)
337         if dest.type != 'branch':
338             misuse('destination for %s is a %s, not a branch'
339                   % (spec_args, dest.type))
340     else:
341         dest = default_loc._replace(path=cleanup_vfs_path(spec.dest))
342
343     if dest.path.startswith(b'/.'):
344         misuse('destination for %s must be a valid branch name' % spec_args)
345
346     debug1('dest: %s\n' % loc_desc(dest))
347     return spec, dest
348
349
350 def resolve_ff(spec, src_repo, dest_repo):
351     src = resolve_src(spec, src_repo)
352     spec_args = spec_msg(spec)
353     if src.type == 'tree':
354         misuse('%s is impossible; can only --append a tree to a branch'
355               % spec_args)
356     if src.type not in ('branch', 'save', 'commit'):
357         misuse('source for %s must be a branch, save, or commit, not %s'
358               % (spec_args, src.type))
359     spec, dest = resolve_branch_dest(spec, src, src_repo, dest_repo)
360     return Target(spec=spec, src=src, dest=dest)
361
362
363 def handle_ff(item, src_repo, writer, opt):
364     assert item.spec.method == 'ff'
365     assert item.src.type in ('branch', 'save', 'commit')
366     src_oidx = hexlify(item.src.hash)
367     dest_oidx = hexlify(item.dest.hash) if item.dest.hash else None
368     if not dest_oidx or dest_oidx in src_repo.rev_list(src_oidx):
369         # Can fast forward.
370         get_random_item(item.spec.src, src_oidx, src_repo, writer, opt)
371         commit_items = parse_commit(get_cat_data(src_repo.cat(src_oidx), b'commit'))
372         return item.src.hash, unhexlify(commit_items.tree)
373     misuse('destination is not an ancestor of source for %s'
374            % spec_msg(item.spec))
375
376
377 def resolve_append(spec, src_repo, dest_repo):
378     src = resolve_src(spec, src_repo)
379     if src.type not in ('branch', 'save', 'commit', 'tree'):
380         misuse('source for %s must be a branch, save, commit, or tree, not %s'
381               % (spec_msg(spec), src.type))
382     spec, dest = resolve_branch_dest(spec, src, src_repo, dest_repo)
383     return Target(spec=spec, src=src, dest=dest)
384
385
386 def handle_append(item, src_repo, writer, opt):
387     assert item.spec.method == 'append'
388     assert item.src.type in ('branch', 'save', 'commit', 'tree')
389     assert item.dest.type == 'branch' or not item.dest.type
390     src_oidx = hexlify(item.src.hash)
391     if item.src.type == 'tree':
392         get_random_item(item.spec.src, src_oidx, src_repo, writer, opt)
393         parent = item.dest.hash
394         msg = b'bup save\n\nGenerated by command:\n%r\n' % compat.get_argvb()
395         userline = b'%s <%s@%s>' % (userfullname(), username(), hostname())
396         now = time.time()
397         commit = writer.new_commit(item.src.hash, parent,
398                                    userline, now, None,
399                                    userline, now, None, msg)
400         return commit, item.src.hash
401     commits = list(src_repo.rev_list(src_oidx))
402     commits.reverse()
403     return append_commits(commits, item.spec.src, item.dest.hash,
404                           src_repo, writer, opt)
405
406
407 def resolve_pick(spec, src_repo, dest_repo):
408     src = resolve_src(spec, src_repo)
409     spec_args = spec_msg(spec)
410     if src.type == 'tree':
411         misuse('%s is impossible; can only --append a tree' % spec_args)
412     if src.type not in ('commit', 'save'):
413         misuse('%s impossible; can only pick a commit or save, not %s'
414               % (spec_args, src.type))
415     if not spec.dest:
416         if src.path.startswith(b'/.tag/'):
417             spec = spec._replace(dest=spec.src)
418         elif src.type == 'save':
419             spec = spec._replace(dest=get_save_branch(src_repo, spec.src))
420     if not spec.dest:
421         misuse('no destination provided for %s', spec_args)
422     dest = find_vfs_item(spec.dest, dest_repo)
423     if not dest:
424         cp = validate_vfs_path(cleanup_vfs_path(spec.dest))
425         dest = default_loc._replace(path=cp)
426     else:
427         if not dest.type == 'branch' and not dest.path.startswith(b'/.tag/'):
428             misuse('%s destination is not a tag or branch' % spec_args)
429         if spec.method == 'pick' \
430            and dest.hash and dest.path.startswith(b'/.tag/'):
431             misuse('cannot overwrite existing tag for %s (requires --force-pick)'
432                   % spec_args)
433     return Target(spec=spec, src=src, dest=dest)
434
435
436 def handle_pick(item, src_repo, writer, opt):
437     assert item.spec.method in ('pick', 'force-pick')
438     assert item.src.type in ('save', 'commit')
439     src_oidx = hexlify(item.src.hash)
440     if item.dest.hash:
441         return append_commit(item.spec.src, src_oidx, item.dest.hash,
442                              src_repo, writer, opt)
443     return append_commit(item.spec.src, src_oidx, None, src_repo, writer, opt)
444
445
446 def resolve_new_tag(spec, src_repo, dest_repo):
447     src = resolve_src(spec, src_repo)
448     spec_args = spec_msg(spec)
449     if not spec.dest and src.path.startswith(b'/.tag/'):
450         spec = spec._replace(dest=src.path)
451     if not spec.dest:
452         misuse('no destination (implicit or explicit) for %s', spec_args)
453     dest = find_vfs_item(spec.dest, dest_repo)
454     if not dest:
455         dest = default_loc._replace(path=cleanup_vfs_path(spec.dest))
456     if not dest.path.startswith(b'/.tag/'):
457         misuse('destination for %s must be a VFS tag' % spec_args)
458     if dest.hash:
459         misuse('cannot overwrite existing tag for %s (requires --replace)'
460               % spec_args)
461     return Target(spec=spec, src=src, dest=dest)
462
463
464 def handle_new_tag(item, src_repo, writer, opt):
465     assert item.spec.method == 'new-tag'
466     assert item.dest.path.startswith(b'/.tag/')
467     get_random_item(item.spec.src, hexlify(item.src.hash),
468                     src_repo, writer, opt)
469     return (item.src.hash,)
470
471
472 def resolve_replace(spec, src_repo, dest_repo):
473     src = resolve_src(spec, src_repo)
474     spec_args = spec_msg(spec)
475     if not spec.dest:
476         if src.path.startswith(b'/.tag/') or src.type == 'branch':
477             spec = spec._replace(dest=spec.src)
478     if not spec.dest:
479         misuse('no destination provided for %s', spec_args)
480     dest = find_vfs_item(spec.dest, dest_repo)
481     if dest:
482         if not dest.type == 'branch' and not dest.path.startswith(b'/.tag/'):
483             misuse('%s impossible; can only overwrite branch or tag'
484                   % spec_args)
485     else:
486         cp = validate_vfs_path(cleanup_vfs_path(spec.dest))
487         dest = default_loc._replace(path=cp)
488     if not dest.path.startswith(b'/.tag/') \
489        and not src.type in ('branch', 'save', 'commit'):
490         misuse('cannot overwrite branch with %s for %s' % (src.type, spec_args))
491     return Target(spec=spec, src=src, dest=dest)
492
493
494 def handle_replace(item, src_repo, writer, opt):
495     assert(item.spec.method == 'replace')
496     if item.dest.path.startswith(b'/.tag/'):
497         get_random_item(item.spec.src, hexlify(item.src.hash),
498                         src_repo, writer, opt)
499         return (item.src.hash,)
500     assert(item.dest.type == 'branch' or not item.dest.type)
501     src_oidx = hexlify(item.src.hash)
502     get_random_item(item.spec.src, src_oidx, src_repo, writer, opt)
503     commit_items = parse_commit(get_cat_data(src_repo.cat(src_oidx), b'commit'))
504     return item.src.hash, unhexlify(commit_items.tree)
505
506
507 def resolve_unnamed(spec, src_repo, dest_repo):
508     if spec.dest:
509         misuse('destination name given for %s' % spec_msg(spec))
510     src = resolve_src(spec, src_repo)
511     return Target(spec=spec, src=src, dest=None)
512
513
514 def handle_unnamed(item, src_repo, writer, opt):
515     get_random_item(item.spec.src, hexlify(item.src.hash),
516                     src_repo, writer, opt)
517     return (None,)
518
519
520 def resolve_targets(specs, src_repo, dest_repo):
521     resolved_items = []
522     common_args = src_repo, dest_repo
523     for spec in specs:
524         debug1('initial-spec: %r\n' % (spec,))
525         if spec.method == 'ff':
526             resolved_items.append(resolve_ff(spec, *common_args))
527         elif spec.method == 'append':
528             resolved_items.append(resolve_append(spec, *common_args))
529         elif spec.method in ('pick', 'force-pick'):
530             resolved_items.append(resolve_pick(spec, *common_args))
531         elif spec.method == 'new-tag':
532             resolved_items.append(resolve_new_tag(spec, *common_args))
533         elif spec.method == 'replace':
534             resolved_items.append(resolve_replace(spec, *common_args))
535         elif spec.method == 'unnamed':
536             resolved_items.append(resolve_unnamed(spec, *common_args))
537         else: # Should be impossible -- prevented by the option parser.
538             assert(False)
539
540     # FIXME: check for prefix overlap?  i.e.:
541     #   bup get --ff foo --ff: baz foo/bar
542     #   bup get --new-tag .tag/foo --new-tag: bar .tag/foo/bar
543
544     # Now that we have all the items, check for duplicate tags.
545     tags_targeted = set()
546     for item in resolved_items:
547         dest_path = item.dest and item.dest.path
548         if dest_path:
549             assert(dest_path.startswith(b'/'))
550             if dest_path.startswith(b'/.tag/'):
551                 if dest_path in tags_targeted:
552                     if item.spec.method not in ('replace', 'force-pick'):
553                         misuse('cannot overwrite tag %s via %s' \
554                               % (path_msg(dest_path), spec_msg(item.spec)))
555                 else:
556                     tags_targeted.add(dest_path)
557     return resolved_items
558
559
560 def log_item(name, type, opt, tree=None, commit=None, tag=None):
561     if tag and opt.print_tags:
562         print(hexstr(tag))
563     if tree and opt.print_trees:
564         print(hexstr(tree))
565     if commit and opt.print_commits:
566         print(hexstr(commit))
567     if opt.verbose:
568         last = ''
569         if type in ('root', 'branch', 'save', 'commit', 'tree'):
570             if not name.endswith(b'/'):
571                 last = '/'
572         log('%s%s\n' % (path_msg(name), last))
573
574 def main(argv):
575     is_reverse = environ.get(b'BUP_SERVER_REVERSE')
576     opt = parse_args(argv)
577     git.check_repo_or_die()
578     if opt.source:
579         opt.source = argv_bytes(opt.source)
580     if opt.bwlimit:
581         client.bwlimit = parse_num(opt.bwlimit)
582     if is_reverse and opt.remote:
583         misuse("don't use -r in reverse mode; it's automatic")
584     if opt.remote or is_reverse:
585         dest_repo = RemoteRepo(opt.remote)
586     else:
587         dest_repo = LocalRepo()
588
589     with dest_repo as dest_repo:
590         with LocalRepo(repo_dir=opt.source) as src_repo:
591             with dest_repo.new_packwriter(compression_level=opt.compress) as writer:
592                 # Resolve and validate all sources and destinations,
593                 # implicit or explicit, and do it up-front, so we can
594                 # fail before we start writing (for any obviously
595                 # broken cases).
596                 target_items = resolve_targets(opt.target_specs,
597                                                src_repo, dest_repo)
598
599                 updated_refs = {}  # ref_name -> (original_ref, tip_commit(bin))
600                 no_ref_info = (None, None)
601
602                 handlers = {'ff': handle_ff,
603                             'append': handle_append,
604                             'force-pick': handle_pick,
605                             'pick': handle_pick,
606                             'new-tag': handle_new_tag,
607                             'replace': handle_replace,
608                             'unnamed': handle_unnamed}
609
610                 for item in target_items:
611                     debug1('get-spec: %r\n' % (item.spec,))
612                     debug1('get-src: %s\n' % loc_desc(item.src))
613                     debug1('get-dest: %s\n' % loc_desc(item.dest))
614                     dest_path = item.dest and item.dest.path
615                     if dest_path:
616                         if dest_path.startswith(b'/.tag/'):
617                             dest_ref = b'refs/tags/%s' % dest_path[6:]
618                         else:
619                             dest_ref = b'refs/heads/%s' % dest_path[1:]
620                     else:
621                         dest_ref = None
622
623                     dest_hash = item.dest and item.dest.hash
624                     orig_ref, cur_ref = updated_refs.get(dest_ref, no_ref_info)
625                     orig_ref = orig_ref or dest_hash
626                     cur_ref = cur_ref or dest_hash
627
628                     handler = handlers[item.spec.method]
629                     item_result = handler(item, src_repo, writer, opt)
630                     if len(item_result) > 1:
631                         new_id, tree = item_result
632                     else:
633                         new_id = item_result[0]
634
635                     if not dest_ref:
636                         log_item(item.spec.src, item.src.type, opt)
637                     else:
638                         updated_refs[dest_ref] = (orig_ref, new_id)
639                         if dest_ref.startswith(b'refs/tags/'):
640                             log_item(item.spec.src, item.src.type, opt, tag=new_id)
641                         else:
642                             log_item(item.spec.src, item.src.type, opt,
643                                      tree=tree, commit=new_id)
644
645         # Only update the refs at the very end, once the writer is
646         # closed, so that if something goes wrong above, the old refs
647         # will be undisturbed.
648         for ref_name, info in items(updated_refs):
649             orig_ref, new_ref = info
650             try:
651                 dest_repo.update_ref(ref_name, new_ref, orig_ref)
652                 if opt.verbose:
653                     new_hex = hexlify(new_ref)
654                     if orig_ref:
655                         orig_hex = hexlify(orig_ref)
656                         log('updated %r (%s -> %s)\n' % (ref_name, orig_hex, new_hex))
657                     else:
658                         log('updated %r (%s)\n' % (ref_name, new_hex))
659             except (git.GitError, client.ClientError) as ex:
660                 add_error('unable to update ref %r: %s' % (ref_name, ex))
661
662     if saved_errors:
663         log('WARNING: %d errors encountered while saving.\n' % len(saved_errors))
664         sys.exit(1)