3 bup_python="$(dirname "$0")/bup-python" || exit $?
4 exec "$bup_python" "$0" ${1+"$@"}
8 from __future__ import absolute_import, print_function
9 from collections import defaultdict
10 from itertools import groupby
11 from sys import stderr
12 from time import localtime, strftime, time
15 from bup import git, options
16 from bup.compat import int_types
17 from bup.gc import bup_gc
18 from bup.helpers import die_if_errors, log, partition, period_as_secs
19 from bup.repo import LocalRepo
20 from bup.rm import bup_rm
23 def branches(refnames=()):
24 return ((name[11:], sha.encode('hex')) for (name,sha)
25 in git.list_refs(patterns=('refs/heads/' + n for n in refnames),
28 def save_name(branch, utc):
29 return branch + '/' + strftime('%Y-%m-%d-%H%M%S', localtime(utc))
31 def classify_saves(saves, period_start):
32 """For each (utc, id) in saves, yield (True, (utc, id)) if the save
33 should be kept and (False, (utc, id)) if the save should be removed.
34 The ids are binary hashes.
37 def retain_newest_in_region(region):
38 for save in region[0:1]:
40 for save in region[1:]:
43 matches, rest = partition(lambda s: s[0] >= period_start['all'], saves)
47 tm_ranges = ((period_start['dailies'], lambda s: localtime(s[0]).tm_yday),
48 (period_start['monthlies'], lambda s: localtime(s[0]).tm_mon),
49 (period_start['yearlies'], lambda s: localtime(s[0]).tm_year))
51 # Break the decreasing utc sorted saves up into the respective
52 # period ranges (dailies, monthlies, ...). Within each range,
53 # group the saves by the period scale (days, months, ...), and
54 # then yield a "keep" action (True, utc) for the newest save in
55 # each group, and a "drop" action (False, utc) for the rest.
56 for pstart, time_region_id in tm_ranges:
57 matches, rest = partition(lambda s: s[0] >= pstart, rest)
58 for region_id, region_saves in groupby(matches, time_region_id):
59 for action in retain_newest_in_region(list(region_saves)):
62 # Finally, drop any saves older than the specified periods
68 bup prune-older [options...] [BRANCH...]
70 keep-all-for= retain all saves within the PERIOD
71 keep-dailies-for= retain the newest save per day within the PERIOD
72 keep-monthlies-for= retain the newest save per month within the PERIOD
73 keep-yearlies-for= retain the newest save per year within the PERIOD
74 wrt= end all periods at this number of seconds since the epoch
75 pretend don't prune, just report intended actions to standard output
76 gc collect garbage after removals [1]
77 gc-threshold= only rewrite a packfile if it's over this percent garbage [10]
78 #,compress= set compression level to # (0-9, 9 is highest) [1]
79 v,verbose increase log output (can be used more than once)
80 unsafe use the command even though it may be DANGEROUS
83 o = options.Options(optspec)
84 opt, flags, roots = o.parse(sys.argv[1:])
87 o.fatal('refusing to run dangerous, experimental command without --unsafe')
89 now = int(time()) if opt.wrt is None else opt.wrt
90 if not isinstance(now, int_types):
91 o.fatal('--wrt value ' + str(now) + ' is not an integer')
94 for period, extent in (('all', opt.keep_all_for),
95 ('dailies', opt.keep_dailies_for),
96 ('monthlies', opt.keep_monthlies_for),
97 ('yearlies', opt.keep_yearlies_for)):
99 secs = period_as_secs(extent)
101 o.fatal('%r is not a valid period' % extent)
102 period_start[period] = now - secs
105 o.fatal('at least one keep argument is required')
107 period_start = defaultdict(lambda: float('inf'), period_start)
110 epoch_ymd = strftime('%Y-%m-%d-%H%M%S', localtime(0))
111 for kind in ['all', 'dailies', 'monthlies', 'yearlies']:
112 period_utc = period_start[kind]
113 if period_utc != float('inf'):
114 if not (period_utc > float('-inf')):
115 log('keeping all ' + kind)
118 when = strftime('%Y-%m-%d-%H%M%S', localtime(period_utc))
119 log('keeping ' + kind + ' since ' + when + '\n')
120 except ValueError as ex:
122 log('keeping %s since %d seconds before %s\n'
123 %(kind, abs(period_utc), epoch_ymd))
125 log('keeping %s since %d seconds after %s\n'
126 %(kind, period_utc, epoch_ymd))
128 log('keeping %s since %s\n' % (kind, epoch_ymd))
130 git.check_repo_or_die()
132 # This could be more efficient, but for now just build the whole list
133 # in memory and let bup_rm() do some redundant work.
136 author_secs = f.readline().strip()
137 return int(author_secs)
140 for branch, branch_id in branches(roots):
142 saves = ((utc, oidx.decode('hex')) for (oidx, utc) in
143 git.rev_list(branch_id, format='%at', parse=parse_info))
144 for keep_save, (utc, id) in classify_saves(saves, period_start):
145 assert(keep_save in (False, True))
146 # FIXME: base removals on hashes
148 print('+' if keep_save else '-', save_name(branch, utc))
150 removals.append(save_name(branch, utc))
155 bup_rm(repo, removals, compression=opt.compress, verbosity=opt.verbose)
158 bup_gc(threshold=opt.gc_threshold,
159 compression=opt.compress,
160 verbosity=opt.verbose)