]> arthur.barton.de Git - bup.git/blob - lib/cmd/prune-older-cmd.py
Bypass Python 3 glibc argv problems by routing args through env
[bup.git] / lib / cmd / prune-older-cmd.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")/bup-python" || exit $?
13 exec "$bup_python" "$0"
14 """
15 # end of bup preamble
16
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 re, sys
24
25 from bup import compat, git, options
26 from bup.compat import argv_bytes, int_types
27 from bup.gc import bup_gc
28 from bup.helpers import die_if_errors, log, partition, period_as_secs
29 from bup.io import byte_stream
30 from bup.repo import LocalRepo
31 from bup.rm import bup_rm
32
33
34 def branches(refnames=tuple()):
35     return ((name[11:], hexlify(sha)) for (name,sha)
36             in git.list_refs(patterns=(b'refs/heads/' + n for n in refnames),
37                              limit_to_heads=True))
38
39 def save_name(branch, utc):
40     return branch + b'/' \
41             + strftime('%Y-%m-%d-%H%M%S', localtime(utc)).encode('ascii')
42
43 def classify_saves(saves, period_start):
44     """For each (utc, id) in saves, yield (True, (utc, id)) if the save
45     should be kept and (False, (utc, id)) if the save should be removed.
46     The ids are binary hashes.
47     """
48
49     def retain_newest_in_region(region):
50         for save in region[0:1]:
51             yield True, save
52         for save in region[1:]:
53             yield False, save
54
55     matches, rest = partition(lambda s: s[0] >= period_start['all'], saves)
56     for save in matches:
57         yield True, save
58
59     tm_ranges = ((period_start['dailies'], lambda s: localtime(s[0]).tm_yday),
60                  (period_start['monthlies'], lambda s: localtime(s[0]).tm_mon),
61                  (period_start['yearlies'], lambda s: localtime(s[0]).tm_year))
62
63     # Break the decreasing utc sorted saves up into the respective
64     # period ranges (dailies, monthlies, ...).  Within each range,
65     # group the saves by the period scale (days, months, ...), and
66     # then yield a "keep" action (True, utc) for the newest save in
67     # each group, and a "drop" action (False, utc) for the rest.
68     for pstart, time_region_id in tm_ranges:
69         matches, rest = partition(lambda s: s[0] >= pstart, rest)
70         for region_id, region_saves in groupby(matches, time_region_id):
71             for action in retain_newest_in_region(list(region_saves)):
72                 yield action
73
74     # Finally, drop any saves older than the specified periods
75     for save in rest:
76         yield False, save
77
78
79 optspec = """
80 bup prune-older [options...] [BRANCH...]
81 --
82 keep-all-for=       retain all saves within the PERIOD
83 keep-dailies-for=   retain the newest save per day within the PERIOD
84 keep-monthlies-for= retain the newest save per month within the PERIOD
85 keep-yearlies-for=  retain the newest save per year within the PERIOD
86 wrt=                end all periods at this number of seconds since the epoch
87 pretend       don't prune, just report intended actions to standard output
88 gc            collect garbage after removals [1]
89 gc-threshold= only rewrite a packfile if it's over this percent garbage [10]
90 #,compress=   set compression level to # (0-9, 9 is highest) [1]
91 v,verbose     increase log output (can be used more than once)
92 unsafe        use the command even though it may be DANGEROUS
93 """
94
95 o = options.Options(optspec)
96 opt, flags, roots = o.parse(compat.argv[1:])
97 roots = [argv_bytes(x) for x in roots]
98
99 if not opt.unsafe:
100     o.fatal('refusing to run dangerous, experimental command without --unsafe')
101
102 now = int(time()) if opt.wrt is None else opt.wrt
103 if not isinstance(now, int_types):
104     o.fatal('--wrt value ' + str(now) + ' is not an integer')
105
106 period_start = {}
107 for period, extent in (('all', opt.keep_all_for),
108                        ('dailies', opt.keep_dailies_for),
109                        ('monthlies', opt.keep_monthlies_for),
110                        ('yearlies', opt.keep_yearlies_for)):
111     if extent:
112         secs = period_as_secs(extent.encode('ascii'))
113         if not secs:
114             o.fatal('%r is not a valid period' % extent)
115         period_start[period] = now - secs
116
117 if not period_start:
118     o.fatal('at least one keep argument is required')
119
120 period_start = defaultdict(lambda: float('inf'), period_start)
121
122 if opt.verbose:
123     epoch_ymd = strftime('%Y-%m-%d-%H%M%S', localtime(0))
124     for kind in ['all', 'dailies', 'monthlies', 'yearlies']:
125         period_utc = period_start[kind]
126         if period_utc != float('inf'):
127             if not (period_utc > float('-inf')):
128                 log('keeping all ' + kind)
129             else:
130                 try:
131                     when = strftime('%Y-%m-%d-%H%M%S', localtime(period_utc))
132                     log('keeping ' + kind + ' since ' + when + '\n')
133                 except ValueError as ex:
134                     if period_utc < 0:
135                         log('keeping %s since %d seconds before %s\n'
136                             %(kind, abs(period_utc), epoch_ymd))
137                     elif period_utc > 0:
138                         log('keeping %s since %d seconds after %s\n'
139                             %(kind, period_utc, epoch_ymd))
140                     else:
141                         log('keeping %s since %s\n' % (kind, epoch_ymd))
142
143 git.check_repo_or_die()
144
145 # This could be more efficient, but for now just build the whole list
146 # in memory and let bup_rm() do some redundant work.
147
148 def parse_info(f):
149     author_secs = f.readline().strip()
150     return int(author_secs)
151
152 sys.stdout.flush()
153 out = byte_stream(sys.stdout)
154
155 removals = []
156 for branch, branch_id in branches(roots):
157     die_if_errors()
158     saves = ((utc, unhexlify(oidx)) for (oidx, utc) in
159              git.rev_list(branch_id, format=b'%at', parse=parse_info))
160     for keep_save, (utc, id) in classify_saves(saves, period_start):
161         assert(keep_save in (False, True))
162         # FIXME: base removals on hashes
163         if opt.pretend:
164             out.write(b'+ ' if keep_save else b'- '
165                       + save_name(branch, utc) + b'\n')
166         elif not keep_save:
167             removals.append(save_name(branch, utc))
168
169 if not opt.pretend:
170     die_if_errors()
171     repo = LocalRepo()
172     bup_rm(repo, removals, compression=opt.compress, verbosity=opt.verbose)
173     if opt.gc:
174         die_if_errors()
175         bup_gc(threshold=opt.gc_threshold,
176                compression=opt.compress,
177                verbosity=opt.verbose)
178
179 die_if_errors()