3 bup_python="$(dirname "$0")/bup-python" || exit $?
4 exec "$bup_python" "$0" ${1+"$@"}
8 from __future__ import absolute_import, print_function
9 from binascii import hexlify, unhexlify
10 from collections import defaultdict
11 from itertools import groupby
12 from sys import stderr
13 from time import localtime, strftime, time
16 from bup import git, options
17 from bup.compat import argv_bytes, int_types
18 from bup.gc import bup_gc
19 from bup.helpers import die_if_errors, log, partition, period_as_secs
20 from bup.io import byte_stream
21 from bup.repo import LocalRepo
22 from bup.rm import bup_rm
25 def branches(refnames=tuple()):
26 return ((name[11:], hexlify(sha)) for (name,sha)
27 in git.list_refs(patterns=(b'refs/heads/' + n for n in refnames),
30 def save_name(branch, utc):
31 return branch + b'/' \
32 + strftime('%Y-%m-%d-%H%M%S', localtime(utc)).encode('ascii')
34 def classify_saves(saves, period_start):
35 """For each (utc, id) in saves, yield (True, (utc, id)) if the save
36 should be kept and (False, (utc, id)) if the save should be removed.
37 The ids are binary hashes.
40 def retain_newest_in_region(region):
41 for save in region[0:1]:
43 for save in region[1:]:
46 matches, rest = partition(lambda s: s[0] >= period_start['all'], saves)
50 tm_ranges = ((period_start['dailies'], lambda s: localtime(s[0]).tm_yday),
51 (period_start['monthlies'], lambda s: localtime(s[0]).tm_mon),
52 (period_start['yearlies'], lambda s: localtime(s[0]).tm_year))
54 # Break the decreasing utc sorted saves up into the respective
55 # period ranges (dailies, monthlies, ...). Within each range,
56 # group the saves by the period scale (days, months, ...), and
57 # then yield a "keep" action (True, utc) for the newest save in
58 # each group, and a "drop" action (False, utc) for the rest.
59 for pstart, time_region_id in tm_ranges:
60 matches, rest = partition(lambda s: s[0] >= pstart, rest)
61 for region_id, region_saves in groupby(matches, time_region_id):
62 for action in retain_newest_in_region(list(region_saves)):
65 # Finally, drop any saves older than the specified periods
71 bup prune-older [options...] [BRANCH...]
73 keep-all-for= retain all saves within the PERIOD
74 keep-dailies-for= retain the newest save per day within the PERIOD
75 keep-monthlies-for= retain the newest save per month within the PERIOD
76 keep-yearlies-for= retain the newest save per year within the PERIOD
77 wrt= end all periods at this number of seconds since the epoch
78 pretend don't prune, just report intended actions to standard output
79 gc collect garbage after removals [1]
80 gc-threshold= only rewrite a packfile if it's over this percent garbage [10]
81 #,compress= set compression level to # (0-9, 9 is highest) [1]
82 v,verbose increase log output (can be used more than once)
83 unsafe use the command even though it may be DANGEROUS
86 o = options.Options(optspec)
87 opt, flags, roots = o.parse(sys.argv[1:])
88 roots = [argv_bytes(x) for x in roots]
91 o.fatal('refusing to run dangerous, experimental command without --unsafe')
93 now = int(time()) if opt.wrt is None else opt.wrt
94 if not isinstance(now, int_types):
95 o.fatal('--wrt value ' + str(now) + ' is not an integer')
98 for period, extent in (('all', opt.keep_all_for),
99 ('dailies', opt.keep_dailies_for),
100 ('monthlies', opt.keep_monthlies_for),
101 ('yearlies', opt.keep_yearlies_for)):
103 secs = period_as_secs(extent.encode('ascii'))
105 o.fatal('%r is not a valid period' % extent)
106 period_start[period] = now - secs
109 o.fatal('at least one keep argument is required')
111 period_start = defaultdict(lambda: float('inf'), period_start)
114 epoch_ymd = strftime('%Y-%m-%d-%H%M%S', localtime(0))
115 for kind in ['all', 'dailies', 'monthlies', 'yearlies']:
116 period_utc = period_start[kind]
117 if period_utc != float('inf'):
118 if not (period_utc > float('-inf')):
119 log('keeping all ' + kind)
122 when = strftime('%Y-%m-%d-%H%M%S', localtime(period_utc))
123 log('keeping ' + kind + ' since ' + when + '\n')
124 except ValueError as ex:
126 log('keeping %s since %d seconds before %s\n'
127 %(kind, abs(period_utc), epoch_ymd))
129 log('keeping %s since %d seconds after %s\n'
130 %(kind, period_utc, epoch_ymd))
132 log('keeping %s since %s\n' % (kind, epoch_ymd))
134 git.check_repo_or_die()
136 # This could be more efficient, but for now just build the whole list
137 # in memory and let bup_rm() do some redundant work.
140 author_secs = f.readline().strip()
141 return int(author_secs)
144 out = byte_stream(sys.stdout)
147 for branch, branch_id in branches(roots):
149 saves = ((utc, unhexlify(oidx)) for (oidx, utc) in
150 git.rev_list(branch_id, format=b'%at', parse=parse_info))
151 for keep_save, (utc, id) in classify_saves(saves, period_start):
152 assert(keep_save in (False, True))
153 # FIXME: base removals on hashes
155 out.write(b'+ ' if keep_save else b'- '
156 + save_name(branch, utc) + b'\n')
158 removals.append(save_name(branch, utc))
163 bup_rm(repo, removals, compression=opt.compress, verbosity=opt.verbose)
166 bup_gc(threshold=opt.gc_threshold,
167 compression=opt.compress,
168 verbosity=opt.verbose)