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