]> arthur.barton.de Git - bup.git/blob - cmd/prune-older-cmd.py
prune-older: accommodate python 3 and test there
[bup.git] / cmd / prune-older-cmd.py
1 #!/bin/sh
2 """": # -*-python-*-
3 bup_python="$(dirname "$0")/bup-python" || exit $?
4 exec "$bup_python" "$0" ${1+"$@"}
5 """
6 # end of bup preamble
7
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
14 import re, sys
15
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
23
24
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),
28                              limit_to_heads=True))
29
30 def save_name(branch, utc):
31     return branch + b'/' \
32             + strftime('%Y-%m-%d-%H%M%S', localtime(utc)).encode('ascii')
33
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.
38     """
39
40     def retain_newest_in_region(region):
41         for save in region[0:1]:
42             yield True, save
43         for save in region[1:]:
44             yield False, save
45
46     matches, rest = partition(lambda s: s[0] >= period_start['all'], saves)
47     for save in matches:
48         yield True, save
49
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))
53
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)):
63                 yield action
64
65     # Finally, drop any saves older than the specified periods
66     for save in rest:
67         yield False, save
68
69
70 optspec = """
71 bup prune-older [options...] [BRANCH...]
72 --
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
84 """
85
86 o = options.Options(optspec)
87 opt, flags, roots = o.parse(sys.argv[1:])
88 roots = [argv_bytes(x) for x in roots]
89
90 if not opt.unsafe:
91     o.fatal('refusing to run dangerous, experimental command without --unsafe')
92
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')
96
97 period_start = {}
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)):
102     if extent:
103         secs = period_as_secs(extent.encode('ascii'))
104         if not secs:
105             o.fatal('%r is not a valid period' % extent)
106         period_start[period] = now - secs
107
108 if not period_start:
109     o.fatal('at least one keep argument is required')
110
111 period_start = defaultdict(lambda: float('inf'), period_start)
112
113 if opt.verbose:
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)
120             else:
121                 try:
122                     when = strftime('%Y-%m-%d-%H%M%S', localtime(period_utc))
123                     log('keeping ' + kind + ' since ' + when + '\n')
124                 except ValueError as ex:
125                     if period_utc < 0:
126                         log('keeping %s since %d seconds before %s\n'
127                             %(kind, abs(period_utc), epoch_ymd))
128                     elif period_utc > 0:
129                         log('keeping %s since %d seconds after %s\n'
130                             %(kind, period_utc, epoch_ymd))
131                     else:
132                         log('keeping %s since %s\n' % (kind, epoch_ymd))
133
134 git.check_repo_or_die()
135
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.
138
139 def parse_info(f):
140     author_secs = f.readline().strip()
141     return int(author_secs)
142
143 sys.stdout.flush()
144 out = byte_stream(sys.stdout)
145
146 removals = []
147 for branch, branch_id in branches(roots):
148     die_if_errors()
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
154         if opt.pretend:
155             out.write(b'+ ' if keep_save else b'- '
156                       + save_name(branch, utc) + b'\n')
157         elif not keep_save:
158             removals.append(save_name(branch, utc))
159
160 if not opt.pretend:
161     die_if_errors()
162     repo = LocalRepo()
163     bup_rm(repo, removals, compression=opt.compress, verbosity=opt.verbose)
164     if opt.gc:
165         die_if_errors()
166         bup_gc(threshold=opt.gc_threshold,
167                compression=opt.compress,
168                verbosity=opt.verbose)
169
170 die_if_errors()