3 # https://sourceware.org/bugzilla/show_bug.cgi?id=26034
4 export "BUP_ARGV_0"="$0"
7 export "BUP_ARGV_${arg_i}"="$arg"
11 # Here to end of preamble replaced during install
12 bup_python="$(dirname "$0")/../../config/bin/python" || exit $?
13 exec "$bup_python" "$0"
17 from __future__ import absolute_import, print_function
18 from binascii import hexlify, unhexlify
19 from collections import defaultdict
20 from itertools import groupby
21 from sys import stderr
22 from time import localtime, strftime, time
23 import os.path, re, sys
25 sys.path[:0] = [os.path.dirname(os.path.realpath(__file__)) + '/..']
27 from bup import compat, git, options
28 from bup.compat import argv_bytes, int_types
29 from bup.gc import bup_gc
30 from bup.helpers import die_if_errors, log, partition, period_as_secs
31 from bup.io import byte_stream
32 from bup.repo import LocalRepo
33 from bup.rm import bup_rm
36 def branches(refnames=tuple()):
37 return ((name[11:], hexlify(sha)) for (name,sha)
38 in git.list_refs(patterns=(b'refs/heads/' + n for n in refnames),
41 def save_name(branch, utc):
42 return branch + b'/' \
43 + strftime('%Y-%m-%d-%H%M%S', localtime(utc)).encode('ascii')
45 def classify_saves(saves, period_start):
46 """For each (utc, id) in saves, yield (True, (utc, id)) if the save
47 should be kept and (False, (utc, id)) if the save should be removed.
48 The ids are binary hashes.
51 def retain_newest_in_region(region):
52 for save in region[0:1]:
54 for save in region[1:]:
57 matches, rest = partition(lambda s: s[0] >= period_start['all'], saves)
61 tm_ranges = ((period_start['dailies'], lambda s: localtime(s[0]).tm_yday),
62 (period_start['monthlies'], lambda s: localtime(s[0]).tm_mon),
63 (period_start['yearlies'], lambda s: localtime(s[0]).tm_year))
65 # Break the decreasing utc sorted saves up into the respective
66 # period ranges (dailies, monthlies, ...). Within each range,
67 # group the saves by the period scale (days, months, ...), and
68 # then yield a "keep" action (True, utc) for the newest save in
69 # each group, and a "drop" action (False, utc) for the rest.
70 for pstart, time_region_id in tm_ranges:
71 matches, rest = partition(lambda s: s[0] >= pstart, rest)
72 for region_id, region_saves in groupby(matches, time_region_id):
73 for action in retain_newest_in_region(list(region_saves)):
76 # Finally, drop any saves older than the specified periods
82 bup prune-older [options...] [BRANCH...]
84 keep-all-for= retain all saves within the PERIOD
85 keep-dailies-for= retain the newest save per day within the PERIOD
86 keep-monthlies-for= retain the newest save per month within the PERIOD
87 keep-yearlies-for= retain the newest save per year within the PERIOD
88 wrt= end all periods at this number of seconds since the epoch
89 pretend don't prune, just report intended actions to standard output
90 gc collect garbage after removals [1]
91 gc-threshold= only rewrite a packfile if it's over this percent garbage [10]
92 #,compress= set compression level to # (0-9, 9 is highest) [1]
93 v,verbose increase log output (can be used more than once)
94 unsafe use the command even though it may be DANGEROUS
97 o = options.Options(optspec)
98 opt, flags, roots = o.parse(compat.argv[1:])
99 roots = [argv_bytes(x) for x in roots]
102 o.fatal('refusing to run dangerous, experimental command without --unsafe')
104 now = int(time()) if opt.wrt is None else opt.wrt
105 if not isinstance(now, int_types):
106 o.fatal('--wrt value ' + str(now) + ' is not an integer')
109 for period, extent in (('all', opt.keep_all_for),
110 ('dailies', opt.keep_dailies_for),
111 ('monthlies', opt.keep_monthlies_for),
112 ('yearlies', opt.keep_yearlies_for)):
114 secs = period_as_secs(extent.encode('ascii'))
116 o.fatal('%r is not a valid period' % extent)
117 period_start[period] = now - secs
120 o.fatal('at least one keep argument is required')
122 period_start = defaultdict(lambda: float('inf'), period_start)
125 epoch_ymd = strftime('%Y-%m-%d-%H%M%S', localtime(0))
126 for kind in ['all', 'dailies', 'monthlies', 'yearlies']:
127 period_utc = period_start[kind]
128 if period_utc != float('inf'):
129 if not (period_utc > float('-inf')):
130 log('keeping all ' + kind)
133 when = strftime('%Y-%m-%d-%H%M%S', localtime(period_utc))
134 log('keeping ' + kind + ' since ' + when + '\n')
135 except ValueError as ex:
137 log('keeping %s since %d seconds before %s\n'
138 %(kind, abs(period_utc), epoch_ymd))
140 log('keeping %s since %d seconds after %s\n'
141 %(kind, period_utc, epoch_ymd))
143 log('keeping %s since %s\n' % (kind, epoch_ymd))
145 git.check_repo_or_die()
147 # This could be more efficient, but for now just build the whole list
148 # in memory and let bup_rm() do some redundant work.
151 author_secs = f.readline().strip()
152 return int(author_secs)
155 out = byte_stream(sys.stdout)
158 for branch, branch_id in branches(roots):
160 saves = ((utc, unhexlify(oidx)) for (oidx, utc) in
161 git.rev_list(branch_id, format=b'%at', parse=parse_info))
162 for keep_save, (utc, id) in classify_saves(saves, period_start):
163 assert(keep_save in (False, True))
164 # FIXME: base removals on hashes
166 out.write(b'+ ' if keep_save else b'- '
167 + save_name(branch, utc) + b'\n')
169 removals.append(save_name(branch, utc))
174 bup_rm(repo, removals, compression=opt.compress, verbosity=opt.verbose)
177 bup_gc(threshold=opt.gc_threshold,
178 compression=opt.compress,
179 verbosity=opt.verbose)