Attachment 'untouch.py'
Download 1 # Copyright 2013 Michael Platings <michael@platin.gs>
2 #
3 # Work supported by The Cambridge Crystallographic Data Centre (www.ccdc.cam.ac.uk)
4 #
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
7
8 """save and restore file modified times.
9
10 This extension is designed to avoid invoking unnecessary builds due to
11 Mercurial touching files during update & merge or rebase.
12
13 This extension can be used in 3 ways:
14
15 1. When running "hg pull" with the "--rebase" option and without any resulting
16 conflicts.
17
18 The extension will automatically restore file modified times without any
19 user input.
20
21 Example::
22
23 hg pull --rebase
24
25 2. To explicitly save file modified times and later restore them.
26
27 Running "hg untouch --save" will record the modified times of files in the
28 working directory whose status is "added" or "modified" as well as files
29 that are modified by ancestor changesets whose phase is "draft" or "secret".
30
31 Running "hg untouch" will restore the modified times recorded by previously
32 running "hg untouch --save".
33
34 Example::
35
36 hg untouch --save
37 hg update tip
38 hg merge
39 hg commit -m "merge"
40 hg untouch
41
42 3. Any time after running "hg pull"
43
44 The user may run "hg untouch --prepull" to restore the modified times of
45 certain files. The files will be those that were modified by ancestor
46 changesets whose phase was "draft" or "secret" at the time "hg pull" was
47 run.
48
49 The prepull modified times are independent of those created by "hg untouch
50 --save" so "hg untouch --prepull" can be used independently of "hg untouch".
51
52 Example::
53
54 hg pull
55 hg update
56 hg merge
57 hg commit -m "merge"
58 hg untouch --prepull
59
60 The extension will never change the modified time of a file if it finds that
61 the file's content has changed since its modified time was recorded.
62
63 == Hooks ==
64
65 hook_pre_pull: Save the modified times of files in draft and secret
66 changesets.
67
68 hook_post_pull: If doing "hg pull --rebase" then restore the file modified
69 times saved by the pre-pull hook.
70
71 == Setup ==
72
73 To enable the extension add the following lines to your hgrc or mercurial.ini::
74
75 [extensions]
76 untouch = /path/to/untouch.py
77
78 [hooks]
79 pre-pull.untouch = python:/path/to/untouch.py:hook_pre_pull
80 post-pull.untouch = python:/path/to/untouch.py:hook_post_pull
81
82 """
83
84 from mercurial import util, commands, phases, scmutil
85 from mercurial.i18n import _
86 import os
87 import cPickle
88 from datetime import datetime, date
89
90
91 def filechecksum(fname):
92 with open(fname, 'rb') as f:
93 return util.sha1(f.read()).hexdigest()
94
95 def add_file_details_to_dict(ui, repo, fname, f2th):
96 fpath = repo.wjoin(fname)
97 if os.path.isfile(fpath):
98 mtime = os.stat(fpath).st_mtime
99 checksum = filechecksum(fpath)
100 ui.debug(_('untouch: recording ') + fname + _(' modified time: ') + str(datetime.fromtimestamp(mtime)) + '\n')
101 f2th[fname] = (mtime, checksum)
102 else:
103 ui.debug('untouch: not a file: ' + fpath + '\n')
104
105 def trackedfiles(ui, repo, ctx, f2th):
106 if not ctx:
107 ui.debug('untouch: trackedfiles not ctx: ' + str(ctx) + ' (returning)\n')
108 return
109 if ctx.phase() == phases.public:
110 ui.debug('untouch: trackedfiles ctx public: ' + str(ctx.description()) + ' (returning)\n')
111 return
112 ui.debug('untouch: trackedfiles adding files from ctx: ' + str(ctx.description()) + '\n')
113 for c in ctx.parents():
114 trackedfiles(ui, repo, c, f2th)
115 for fname in ctx.files():
116 add_file_details_to_dict(ui, repo, fname, f2th)
117
118 def gettrackedfiles(ui, repo):
119 f2th = {}
120 for ctx in repo.parents():
121 trackedfiles(ui, repo, ctx, f2th)
122 return f2th
123
124 def workingdirfiles(ui, repo, f2th, savemodified, saveadded, saveclean, saveunknown, saveignored):
125 modified, added, removed, deleted, unknown, ignored, clean = repo.status(clean=saveclean, unknown=saveunknown, ignored=saveignored)
126 filelists = []
127 if savemodified: filelists.append(modified)
128 if saveadded: filelists.append(added)
129 if saveclean: filelists.append(clean)
130 if saveunknown: filelists.append(unknown)
131 if saveignored: filelists.append(ignored)
132
133 for filelist in filelists:
134 for fname in filelist:
135 add_file_details_to_dict(ui, repo, fname, f2th)
136
137 def fileschangedinrev(ui, repo, f2th, rev):
138 for fname in repo[rev].files():
139 add_file_details_to_dict(ui, repo, fname, f2th)
140
141 def notechangingfiletime(ui, fname, mtime1, mtime2):
142 today = date.today()
143 mtime1 = datetime.fromtimestamp(mtime1)
144 mtime2 = datetime.fromtimestamp(mtime2)
145
146 timeformat = '%H:%M:%S'
147 datetimeformat = '%Y-%m-%d ' + timeformat
148
149 mtime1 = mtime1.strftime(timeformat if mtime1.date() == today else datetimeformat)
150 mtime2 = mtime2.strftime(timeformat if mtime2.date() == today else datetimeformat)
151
152 ui.note('untouch: ' + _('changing modified time of ') + fname + _(' from ') + mtime1 + _(' to ') + mtime2 + '\n')
153
154 def getmanualdatapath(repo):
155 return os.path.join(repo.join('untouch'), 'saved')
156
157 def getpulldatapath(repo):
158 return os.path.join(repo.join('untouch'), 'prepull')
159
160 def createdatadir(repo):
161 datadir = repo.join('untouch')
162 if not os.path.exists(datadir):
163 os.mkdir(datadir)
164
165 def restore_timestamps_from_file(ui, repo, filetimespath):
166 with open(filetimespath) as f:
167 f2th = cPickle.load(f)
168
169 if type(f2th) is not dict:
170 ui.warn(_('untouch: data file invalid\n'))
171 return
172
173 ui.debug('untouch: restorefiletimes f2th:' + str(f2th) + '\n')
174 for fname, th in f2th.iteritems():
175 fpath = repo.wjoin(fname)
176 timestamp, hash = th
177 if os.path.isfile(fpath) and filechecksum(fpath) == hash:
178 st = os.stat(fpath)
179 if st.st_mtime != timestamp:
180 notechangingfiletime(ui, fname, st.st_mtime, timestamp)
181 os.utime(fpath, (st.st_atime, timestamp))
182
183 def hook_pre_pull(ui, repo, hooktype, **kwargs):
184 createdatadir(repo)
185 with open(getpulldatapath(repo), 'w') as f:
186 f2th = gettrackedfiles(ui, repo)
187 ui.debug('untouch: f2th:' + str(f2th) + '\n')
188 cPickle.dump(f2th, f)
189
190 def hook_post_pull(ui, repo, hooktype, **kwargs):
191 opts = kwargs['opts']
192 if 'rebase' in opts and opts['rebase']:
193 restore_timestamps_from_file(ui, repo, getpulldatapath(repo))
194
195 def untouch(ui, repo, filename=None, **opts):
196 """save or restore file modified times.
197
198 With one or more of -srmacui options, save the corresponding file modified
199 times. If the DEST argument is provided then output to that, otherwise to
200 .hg/untouch/saved.
201
202 With no arguments, load and restore the file modified times from
203 .hg/untouch/saved.
204
205 With the SOURCE argument, load and restore the file modified times from
206 that file.
207
208 If the pre-pull hook (run "hg help untouch -e" for more info) has been set
209 up, it will automatically save modified times to .hg/untouch/prepull. The
210 -p option may be used to restore these times.
211 """
212
213 savedefault = opts['save']
214 savemodified = opts['modified'] or savedefault
215 saveadded = opts['added'] or savedefault
216 saveclean = opts['clean']
217 saveunknown = opts['unknown']
218 saveignored = opts['ignored']
219 saverevs = opts['rev']
220 if savedefault:
221 saverevs.append('not public() and ancestors(.)')
222 saveworkingdir = savemodified or saveadded or saveclean or saveunknown or saveignored
223 save = saveworkingdir or saverevs
224 loadprepull = opts['prepull']
225 load = not save and not loadprepull
226
227 if save and (loadprepull or load):
228 raise util.Abort(_("cannot specify both saving and loading options"))
229
230 if loadprepull and filename:
231 raise util.Abort(_("cannot specify loading both from a file and from pre-pull autosave"))
232
233 if save:
234 if not filename:
235 createdatadir(repo)
236 filename = getmanualdatapath(repo)
237 ui.debug('untouch: saving to ' + filename + '\n')
238
239 with open(filename, 'w') as f:
240 f2th = {}
241 if saverevs:
242 for rev in scmutil.revrange(repo, saverevs):
243 ui.debug('untouch: saving modified times for files modified by revision ' + str(rev) + '\n')
244 fileschangedinrev(ui, repo, f2th, rev)
245 if saveworkingdir:
246 ui.debug('untouch: saving modified times for files in the working directory')
247 workingdirfiles(ui, repo, f2th, savemodified, saveadded, saveclean, saveunknown, saveignored)
248
249 ui.debug('untouch: f2th:' + str(f2th) + '\n')
250 cPickle.dump(f2th, f)
251 else:
252 if loadprepull:
253 filename = getpulldatapath(repo)
254 elif not filename:
255 filename = getmanualdatapath(repo)
256 ui.debug('untouch: loading from ' + filename + '\n')
257 restore_timestamps_from_file(ui, repo, filename)
258
259 cmdtable = {
260 'untouch': (untouch,
261 [('s', 'save', False, _('save modified times of added & modified files and files changed in non-public ancestor changesets. Equivalent to -mar "not public() and ancestors(.)"')),
262 ('r', 'rev', [], _('save modified times of files in the specified revision set'), _('REV')),
263 ('m', 'modified', False, _('save modified times of modified files')),
264 ('a', 'added', False, _('save modified times of added files')),
265 ('c', 'clean', False, _('save modified times of files without changes')),
266 ('u', 'unknown', False, _('save modified times of unknown (not tracked) files')),
267 ('i', 'ignored', False, _('save modified times of ignored files')),
268 ('p', 'prepull', False, _('restore modified times that get automatically recorded before "hg pull"'))],
269 '[([-s] [-m] [-a] [-c] [-u] [-i] [-r REV] [DEST]) | -p | SOURCE]')
270 }
271
272 testedwith = '2.3.2'
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.