##// END OF EJS Templates
utils: stop using datetime.utcfromtimestamp() deprecated in Python 3.12...
Mads Kiilerich -
r51645:faccec1e stable
parent child Browse files
Show More
@@ -1,574 +1,576 b''
1 # common.py - common code for the convert extension
1 # common.py - common code for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Olivia Mackall <olivia@selenic.com> and others
3 # Copyright 2005-2009 Olivia Mackall <olivia@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import base64
8 import base64
9 import datetime
9 import datetime
10 import os
10 import os
11 import pickle
11 import pickle
12 import re
12 import re
13 import shlex
13 import shlex
14 import subprocess
14 import subprocess
15
15
16 from mercurial.i18n import _
16 from mercurial.i18n import _
17 from mercurial.pycompat import open
17 from mercurial.pycompat import open
18 from mercurial import (
18 from mercurial import (
19 encoding,
19 encoding,
20 error,
20 error,
21 phases,
21 phases,
22 pycompat,
22 pycompat,
23 util,
23 util,
24 )
24 )
25 from mercurial.utils import procutil
25 from mercurial.utils import procutil
26
26
27 propertycache = util.propertycache
27 propertycache = util.propertycache
28
28
29
29
30 def _encodeornone(d):
30 def _encodeornone(d):
31 if d is None:
31 if d is None:
32 return
32 return
33 return d.encode('latin1')
33 return d.encode('latin1')
34
34
35
35
36 class _shlexpy3proxy:
36 class _shlexpy3proxy:
37 def __init__(self, l):
37 def __init__(self, l):
38 self._l = l
38 self._l = l
39
39
40 def __iter__(self):
40 def __iter__(self):
41 return (_encodeornone(v) for v in self._l)
41 return (_encodeornone(v) for v in self._l)
42
42
43 def get_token(self):
43 def get_token(self):
44 return _encodeornone(self._l.get_token())
44 return _encodeornone(self._l.get_token())
45
45
46 @property
46 @property
47 def infile(self):
47 def infile(self):
48 return self._l.infile or b'<unknown>'
48 return self._l.infile or b'<unknown>'
49
49
50 @property
50 @property
51 def lineno(self):
51 def lineno(self):
52 return self._l.lineno
52 return self._l.lineno
53
53
54
54
55 def shlexer(data=None, filepath=None, wordchars=None, whitespace=None):
55 def shlexer(data=None, filepath=None, wordchars=None, whitespace=None):
56 if data is None:
56 if data is None:
57 data = open(filepath, b'r', encoding='latin1')
57 data = open(filepath, b'r', encoding='latin1')
58 else:
58 else:
59 if filepath is not None:
59 if filepath is not None:
60 raise error.ProgrammingError(
60 raise error.ProgrammingError(
61 b'shlexer only accepts data or filepath, not both'
61 b'shlexer only accepts data or filepath, not both'
62 )
62 )
63 data = data.decode('latin1')
63 data = data.decode('latin1')
64 l = shlex.shlex(data, infile=filepath, posix=True)
64 l = shlex.shlex(data, infile=filepath, posix=True)
65 if whitespace is not None:
65 if whitespace is not None:
66 l.whitespace_split = True
66 l.whitespace_split = True
67 l.whitespace += whitespace.decode('latin1')
67 l.whitespace += whitespace.decode('latin1')
68 if wordchars is not None:
68 if wordchars is not None:
69 l.wordchars += wordchars.decode('latin1')
69 l.wordchars += wordchars.decode('latin1')
70 return _shlexpy3proxy(l)
70 return _shlexpy3proxy(l)
71
71
72
72
73 def encodeargs(args):
73 def encodeargs(args):
74 def encodearg(s):
74 def encodearg(s):
75 lines = base64.encodebytes(s)
75 lines = base64.encodebytes(s)
76 lines = [l.splitlines()[0] for l in pycompat.iterbytestr(lines)]
76 lines = [l.splitlines()[0] for l in pycompat.iterbytestr(lines)]
77 return b''.join(lines)
77 return b''.join(lines)
78
78
79 s = pickle.dumps(args)
79 s = pickle.dumps(args)
80 return encodearg(s)
80 return encodearg(s)
81
81
82
82
83 def decodeargs(s):
83 def decodeargs(s):
84 s = base64.decodebytes(s)
84 s = base64.decodebytes(s)
85 return pickle.loads(s)
85 return pickle.loads(s)
86
86
87
87
88 class MissingTool(Exception):
88 class MissingTool(Exception):
89 pass
89 pass
90
90
91
91
92 def checktool(exe, name=None, abort=True):
92 def checktool(exe, name=None, abort=True):
93 name = name or exe
93 name = name or exe
94 if not procutil.findexe(exe):
94 if not procutil.findexe(exe):
95 if abort:
95 if abort:
96 exc = error.Abort
96 exc = error.Abort
97 else:
97 else:
98 exc = MissingTool
98 exc = MissingTool
99 raise exc(_(b'cannot find required "%s" tool') % name)
99 raise exc(_(b'cannot find required "%s" tool') % name)
100
100
101
101
102 class NoRepo(Exception):
102 class NoRepo(Exception):
103 pass
103 pass
104
104
105
105
106 SKIPREV = b'SKIP'
106 SKIPREV = b'SKIP'
107
107
108
108
109 class commit:
109 class commit:
110 def __init__(
110 def __init__(
111 self,
111 self,
112 author,
112 author,
113 date,
113 date,
114 desc,
114 desc,
115 parents,
115 parents,
116 branch=None,
116 branch=None,
117 rev=None,
117 rev=None,
118 extra=None,
118 extra=None,
119 sortkey=None,
119 sortkey=None,
120 saverev=True,
120 saverev=True,
121 phase=phases.draft,
121 phase=phases.draft,
122 optparents=None,
122 optparents=None,
123 ctx=None,
123 ctx=None,
124 ):
124 ):
125 self.author = author or b'unknown'
125 self.author = author or b'unknown'
126 self.date = date or b'0 0'
126 self.date = date or b'0 0'
127 self.desc = desc
127 self.desc = desc
128 self.parents = parents # will be converted and used as parents
128 self.parents = parents # will be converted and used as parents
129 self.optparents = optparents or [] # will be used if already converted
129 self.optparents = optparents or [] # will be used if already converted
130 self.branch = branch
130 self.branch = branch
131 self.rev = rev
131 self.rev = rev
132 self.extra = extra or {}
132 self.extra = extra or {}
133 self.sortkey = sortkey
133 self.sortkey = sortkey
134 self.saverev = saverev
134 self.saverev = saverev
135 self.phase = phase
135 self.phase = phase
136 self.ctx = ctx # for hg to hg conversions
136 self.ctx = ctx # for hg to hg conversions
137
137
138
138
139 class converter_source:
139 class converter_source:
140 """Conversion source interface"""
140 """Conversion source interface"""
141
141
142 def __init__(self, ui, repotype, path=None, revs=None):
142 def __init__(self, ui, repotype, path=None, revs=None):
143 """Initialize conversion source (or raise NoRepo("message")
143 """Initialize conversion source (or raise NoRepo("message")
144 exception if path is not a valid repository)"""
144 exception if path is not a valid repository)"""
145 self.ui = ui
145 self.ui = ui
146 self.path = path
146 self.path = path
147 self.revs = revs
147 self.revs = revs
148 self.repotype = repotype
148 self.repotype = repotype
149
149
150 self.encoding = b'utf-8'
150 self.encoding = b'utf-8'
151
151
152 def checkhexformat(self, revstr, mapname=b'splicemap'):
152 def checkhexformat(self, revstr, mapname=b'splicemap'):
153 """fails if revstr is not a 40 byte hex. mercurial and git both uses
153 """fails if revstr is not a 40 byte hex. mercurial and git both uses
154 such format for their revision numbering
154 such format for their revision numbering
155 """
155 """
156 if not re.match(br'[0-9a-fA-F]{40,40}$', revstr):
156 if not re.match(br'[0-9a-fA-F]{40,40}$', revstr):
157 raise error.Abort(
157 raise error.Abort(
158 _(b'%s entry %s is not a valid revision identifier')
158 _(b'%s entry %s is not a valid revision identifier')
159 % (mapname, revstr)
159 % (mapname, revstr)
160 )
160 )
161
161
162 def before(self):
162 def before(self):
163 pass
163 pass
164
164
165 def after(self):
165 def after(self):
166 pass
166 pass
167
167
168 def targetfilebelongstosource(self, targetfilename):
168 def targetfilebelongstosource(self, targetfilename):
169 """Returns true if the given targetfile belongs to the source repo. This
169 """Returns true if the given targetfile belongs to the source repo. This
170 is useful when only a subdirectory of the target belongs to the source
170 is useful when only a subdirectory of the target belongs to the source
171 repo."""
171 repo."""
172 # For normal full repo converts, this is always True.
172 # For normal full repo converts, this is always True.
173 return True
173 return True
174
174
175 def setrevmap(self, revmap):
175 def setrevmap(self, revmap):
176 """set the map of already-converted revisions"""
176 """set the map of already-converted revisions"""
177
177
178 def getheads(self):
178 def getheads(self):
179 """Return a list of this repository's heads"""
179 """Return a list of this repository's heads"""
180 raise NotImplementedError
180 raise NotImplementedError
181
181
182 def getfile(self, name, rev):
182 def getfile(self, name, rev):
183 """Return a pair (data, mode) where data is the file content
183 """Return a pair (data, mode) where data is the file content
184 as a string and mode one of '', 'x' or 'l'. rev is the
184 as a string and mode one of '', 'x' or 'l'. rev is the
185 identifier returned by a previous call to getchanges().
185 identifier returned by a previous call to getchanges().
186 Data is None if file is missing/deleted in rev.
186 Data is None if file is missing/deleted in rev.
187 """
187 """
188 raise NotImplementedError
188 raise NotImplementedError
189
189
190 def getchanges(self, version, full):
190 def getchanges(self, version, full):
191 """Returns a tuple of (files, copies, cleanp2).
191 """Returns a tuple of (files, copies, cleanp2).
192
192
193 files is a sorted list of (filename, id) tuples for all files
193 files is a sorted list of (filename, id) tuples for all files
194 changed between version and its first parent returned by
194 changed between version and its first parent returned by
195 getcommit(). If full, all files in that revision is returned.
195 getcommit(). If full, all files in that revision is returned.
196 id is the source revision id of the file.
196 id is the source revision id of the file.
197
197
198 copies is a dictionary of dest: source
198 copies is a dictionary of dest: source
199
199
200 cleanp2 is the set of files filenames that are clean against p2.
200 cleanp2 is the set of files filenames that are clean against p2.
201 (Files that are clean against p1 are already not in files (unless
201 (Files that are clean against p1 are already not in files (unless
202 full). This makes it possible to handle p2 clean files similarly.)
202 full). This makes it possible to handle p2 clean files similarly.)
203 """
203 """
204 raise NotImplementedError
204 raise NotImplementedError
205
205
206 def getcommit(self, version):
206 def getcommit(self, version):
207 """Return the commit object for version"""
207 """Return the commit object for version"""
208 raise NotImplementedError
208 raise NotImplementedError
209
209
210 def numcommits(self):
210 def numcommits(self):
211 """Return the number of commits in this source.
211 """Return the number of commits in this source.
212
212
213 If unknown, return None.
213 If unknown, return None.
214 """
214 """
215 return None
215 return None
216
216
217 def gettags(self):
217 def gettags(self):
218 """Return the tags as a dictionary of name: revision
218 """Return the tags as a dictionary of name: revision
219
219
220 Tag names must be UTF-8 strings.
220 Tag names must be UTF-8 strings.
221 """
221 """
222 raise NotImplementedError
222 raise NotImplementedError
223
223
224 def recode(self, s, encoding=None):
224 def recode(self, s, encoding=None):
225 if not encoding:
225 if not encoding:
226 encoding = self.encoding or b'utf-8'
226 encoding = self.encoding or b'utf-8'
227
227
228 if isinstance(s, str):
228 if isinstance(s, str):
229 return s.encode("utf-8")
229 return s.encode("utf-8")
230 try:
230 try:
231 return s.decode(pycompat.sysstr(encoding)).encode("utf-8")
231 return s.decode(pycompat.sysstr(encoding)).encode("utf-8")
232 except UnicodeError:
232 except UnicodeError:
233 try:
233 try:
234 return s.decode("latin-1").encode("utf-8")
234 return s.decode("latin-1").encode("utf-8")
235 except UnicodeError:
235 except UnicodeError:
236 return s.decode(pycompat.sysstr(encoding), "replace").encode(
236 return s.decode(pycompat.sysstr(encoding), "replace").encode(
237 "utf-8"
237 "utf-8"
238 )
238 )
239
239
240 def getchangedfiles(self, rev, i):
240 def getchangedfiles(self, rev, i):
241 """Return the files changed by rev compared to parent[i].
241 """Return the files changed by rev compared to parent[i].
242
242
243 i is an index selecting one of the parents of rev. The return
243 i is an index selecting one of the parents of rev. The return
244 value should be the list of files that are different in rev and
244 value should be the list of files that are different in rev and
245 this parent.
245 this parent.
246
246
247 If rev has no parents, i is None.
247 If rev has no parents, i is None.
248
248
249 This function is only needed to support --filemap
249 This function is only needed to support --filemap
250 """
250 """
251 raise NotImplementedError
251 raise NotImplementedError
252
252
253 def converted(self, rev, sinkrev):
253 def converted(self, rev, sinkrev):
254 '''Notify the source that a revision has been converted.'''
254 '''Notify the source that a revision has been converted.'''
255
255
256 def hasnativeorder(self):
256 def hasnativeorder(self):
257 """Return true if this source has a meaningful, native revision
257 """Return true if this source has a meaningful, native revision
258 order. For instance, Mercurial revisions are store sequentially
258 order. For instance, Mercurial revisions are store sequentially
259 while there is no such global ordering with Darcs.
259 while there is no such global ordering with Darcs.
260 """
260 """
261 return False
261 return False
262
262
263 def hasnativeclose(self):
263 def hasnativeclose(self):
264 """Return true if this source has ability to close branch."""
264 """Return true if this source has ability to close branch."""
265 return False
265 return False
266
266
267 def lookuprev(self, rev):
267 def lookuprev(self, rev):
268 """If rev is a meaningful revision reference in source, return
268 """If rev is a meaningful revision reference in source, return
269 the referenced identifier in the same format used by getcommit().
269 the referenced identifier in the same format used by getcommit().
270 return None otherwise.
270 return None otherwise.
271 """
271 """
272 return None
272 return None
273
273
274 def getbookmarks(self):
274 def getbookmarks(self):
275 """Return the bookmarks as a dictionary of name: revision
275 """Return the bookmarks as a dictionary of name: revision
276
276
277 Bookmark names are to be UTF-8 strings.
277 Bookmark names are to be UTF-8 strings.
278 """
278 """
279 return {}
279 return {}
280
280
281 def checkrevformat(self, revstr, mapname=b'splicemap'):
281 def checkrevformat(self, revstr, mapname=b'splicemap'):
282 """revstr is a string that describes a revision in the given
282 """revstr is a string that describes a revision in the given
283 source control system. Return true if revstr has correct
283 source control system. Return true if revstr has correct
284 format.
284 format.
285 """
285 """
286 return True
286 return True
287
287
288
288
289 class converter_sink:
289 class converter_sink:
290 """Conversion sink (target) interface"""
290 """Conversion sink (target) interface"""
291
291
292 def __init__(self, ui, repotype, path):
292 def __init__(self, ui, repotype, path):
293 """Initialize conversion sink (or raise NoRepo("message")
293 """Initialize conversion sink (or raise NoRepo("message")
294 exception if path is not a valid repository)
294 exception if path is not a valid repository)
295
295
296 created is a list of paths to remove if a fatal error occurs
296 created is a list of paths to remove if a fatal error occurs
297 later"""
297 later"""
298 self.ui = ui
298 self.ui = ui
299 self.path = path
299 self.path = path
300 self.created = []
300 self.created = []
301 self.repotype = repotype
301 self.repotype = repotype
302
302
303 def revmapfile(self):
303 def revmapfile(self):
304 """Path to a file that will contain lines
304 """Path to a file that will contain lines
305 source_rev_id sink_rev_id
305 source_rev_id sink_rev_id
306 mapping equivalent revision identifiers for each system."""
306 mapping equivalent revision identifiers for each system."""
307 raise NotImplementedError
307 raise NotImplementedError
308
308
309 def authorfile(self):
309 def authorfile(self):
310 """Path to a file that will contain lines
310 """Path to a file that will contain lines
311 srcauthor=dstauthor
311 srcauthor=dstauthor
312 mapping equivalent authors identifiers for each system."""
312 mapping equivalent authors identifiers for each system."""
313 return None
313 return None
314
314
315 def putcommit(
315 def putcommit(
316 self, files, copies, parents, commit, source, revmap, full, cleanp2
316 self, files, copies, parents, commit, source, revmap, full, cleanp2
317 ):
317 ):
318 """Create a revision with all changed files listed in 'files'
318 """Create a revision with all changed files listed in 'files'
319 and having listed parents. 'commit' is a commit object
319 and having listed parents. 'commit' is a commit object
320 containing at a minimum the author, date, and message for this
320 containing at a minimum the author, date, and message for this
321 changeset. 'files' is a list of (path, version) tuples,
321 changeset. 'files' is a list of (path, version) tuples,
322 'copies' is a dictionary mapping destinations to sources,
322 'copies' is a dictionary mapping destinations to sources,
323 'source' is the source repository, and 'revmap' is a mapfile
323 'source' is the source repository, and 'revmap' is a mapfile
324 of source revisions to converted revisions. Only getfile() and
324 of source revisions to converted revisions. Only getfile() and
325 lookuprev() should be called on 'source'. 'full' means that 'files'
325 lookuprev() should be called on 'source'. 'full' means that 'files'
326 is complete and all other files should be removed.
326 is complete and all other files should be removed.
327 'cleanp2' is a set of the filenames that are unchanged from p2
327 'cleanp2' is a set of the filenames that are unchanged from p2
328 (only in the common merge case where there two parents).
328 (only in the common merge case where there two parents).
329
329
330 Note that the sink repository is not told to update itself to
330 Note that the sink repository is not told to update itself to
331 a particular revision (or even what that revision would be)
331 a particular revision (or even what that revision would be)
332 before it receives the file data.
332 before it receives the file data.
333 """
333 """
334 raise NotImplementedError
334 raise NotImplementedError
335
335
336 def puttags(self, tags):
336 def puttags(self, tags):
337 """Put tags into sink.
337 """Put tags into sink.
338
338
339 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
339 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
340 Return a pair (tag_revision, tag_parent_revision), or (None, None)
340 Return a pair (tag_revision, tag_parent_revision), or (None, None)
341 if nothing was changed.
341 if nothing was changed.
342 """
342 """
343 raise NotImplementedError
343 raise NotImplementedError
344
344
345 def setbranch(self, branch, pbranches):
345 def setbranch(self, branch, pbranches):
346 """Set the current branch name. Called before the first putcommit
346 """Set the current branch name. Called before the first putcommit
347 on the branch.
347 on the branch.
348 branch: branch name for subsequent commits
348 branch: branch name for subsequent commits
349 pbranches: (converted parent revision, parent branch) tuples"""
349 pbranches: (converted parent revision, parent branch) tuples"""
350
350
351 def setfilemapmode(self, active):
351 def setfilemapmode(self, active):
352 """Tell the destination that we're using a filemap
352 """Tell the destination that we're using a filemap
353
353
354 Some converter_sources (svn in particular) can claim that a file
354 Some converter_sources (svn in particular) can claim that a file
355 was changed in a revision, even if there was no change. This method
355 was changed in a revision, even if there was no change. This method
356 tells the destination that we're using a filemap and that it should
356 tells the destination that we're using a filemap and that it should
357 filter empty revisions.
357 filter empty revisions.
358 """
358 """
359
359
360 def before(self):
360 def before(self):
361 pass
361 pass
362
362
363 def after(self):
363 def after(self):
364 pass
364 pass
365
365
366 def putbookmarks(self, bookmarks):
366 def putbookmarks(self, bookmarks):
367 """Put bookmarks into sink.
367 """Put bookmarks into sink.
368
368
369 bookmarks: {bookmarkname: sink_rev_id, ...}
369 bookmarks: {bookmarkname: sink_rev_id, ...}
370 where bookmarkname is an UTF-8 string.
370 where bookmarkname is an UTF-8 string.
371 """
371 """
372
372
373 def hascommitfrommap(self, rev):
373 def hascommitfrommap(self, rev):
374 """Return False if a rev mentioned in a filemap is known to not be
374 """Return False if a rev mentioned in a filemap is known to not be
375 present."""
375 present."""
376 raise NotImplementedError
376 raise NotImplementedError
377
377
378 def hascommitforsplicemap(self, rev):
378 def hascommitforsplicemap(self, rev):
379 """This method is for the special needs for splicemap handling and not
379 """This method is for the special needs for splicemap handling and not
380 for general use. Returns True if the sink contains rev, aborts on some
380 for general use. Returns True if the sink contains rev, aborts on some
381 special cases."""
381 special cases."""
382 raise NotImplementedError
382 raise NotImplementedError
383
383
384
384
385 class commandline:
385 class commandline:
386 def __init__(self, ui, command):
386 def __init__(self, ui, command):
387 self.ui = ui
387 self.ui = ui
388 self.command = command
388 self.command = command
389
389
390 def prerun(self):
390 def prerun(self):
391 pass
391 pass
392
392
393 def postrun(self):
393 def postrun(self):
394 pass
394 pass
395
395
396 def _cmdline(self, cmd, *args, **kwargs):
396 def _cmdline(self, cmd, *args, **kwargs):
397 kwargs = pycompat.byteskwargs(kwargs)
397 kwargs = pycompat.byteskwargs(kwargs)
398 cmdline = [self.command, cmd] + list(args)
398 cmdline = [self.command, cmd] + list(args)
399 for k, v in kwargs.items():
399 for k, v in kwargs.items():
400 if len(k) == 1:
400 if len(k) == 1:
401 cmdline.append(b'-' + k)
401 cmdline.append(b'-' + k)
402 else:
402 else:
403 cmdline.append(b'--' + k.replace(b'_', b'-'))
403 cmdline.append(b'--' + k.replace(b'_', b'-'))
404 try:
404 try:
405 if len(k) == 1:
405 if len(k) == 1:
406 cmdline.append(b'' + v)
406 cmdline.append(b'' + v)
407 else:
407 else:
408 cmdline[-1] += b'=' + v
408 cmdline[-1] += b'=' + v
409 except TypeError:
409 except TypeError:
410 pass
410 pass
411 cmdline = [procutil.shellquote(arg) for arg in cmdline]
411 cmdline = [procutil.shellquote(arg) for arg in cmdline]
412 if not self.ui.debugflag:
412 if not self.ui.debugflag:
413 cmdline += [b'2>', pycompat.bytestr(os.devnull)]
413 cmdline += [b'2>', pycompat.bytestr(os.devnull)]
414 cmdline = b' '.join(cmdline)
414 cmdline = b' '.join(cmdline)
415 return cmdline
415 return cmdline
416
416
417 def _run(self, cmd, *args, **kwargs):
417 def _run(self, cmd, *args, **kwargs):
418 def popen(cmdline):
418 def popen(cmdline):
419 p = subprocess.Popen(
419 p = subprocess.Popen(
420 procutil.tonativestr(cmdline),
420 procutil.tonativestr(cmdline),
421 shell=True,
421 shell=True,
422 bufsize=-1,
422 bufsize=-1,
423 close_fds=procutil.closefds,
423 close_fds=procutil.closefds,
424 stdout=subprocess.PIPE,
424 stdout=subprocess.PIPE,
425 )
425 )
426 return p
426 return p
427
427
428 return self._dorun(popen, cmd, *args, **kwargs)
428 return self._dorun(popen, cmd, *args, **kwargs)
429
429
430 def _run2(self, cmd, *args, **kwargs):
430 def _run2(self, cmd, *args, **kwargs):
431 return self._dorun(procutil.popen2, cmd, *args, **kwargs)
431 return self._dorun(procutil.popen2, cmd, *args, **kwargs)
432
432
433 def _run3(self, cmd, *args, **kwargs):
433 def _run3(self, cmd, *args, **kwargs):
434 return self._dorun(procutil.popen3, cmd, *args, **kwargs)
434 return self._dorun(procutil.popen3, cmd, *args, **kwargs)
435
435
436 def _dorun(self, openfunc, cmd, *args, **kwargs):
436 def _dorun(self, openfunc, cmd, *args, **kwargs):
437 cmdline = self._cmdline(cmd, *args, **kwargs)
437 cmdline = self._cmdline(cmd, *args, **kwargs)
438 self.ui.debug(b'running: %s\n' % (cmdline,))
438 self.ui.debug(b'running: %s\n' % (cmdline,))
439 self.prerun()
439 self.prerun()
440 try:
440 try:
441 return openfunc(cmdline)
441 return openfunc(cmdline)
442 finally:
442 finally:
443 self.postrun()
443 self.postrun()
444
444
445 def run(self, cmd, *args, **kwargs):
445 def run(self, cmd, *args, **kwargs):
446 p = self._run(cmd, *args, **kwargs)
446 p = self._run(cmd, *args, **kwargs)
447 output = p.communicate()[0]
447 output = p.communicate()[0]
448 self.ui.debug(output)
448 self.ui.debug(output)
449 return output, p.returncode
449 return output, p.returncode
450
450
451 def runlines(self, cmd, *args, **kwargs):
451 def runlines(self, cmd, *args, **kwargs):
452 p = self._run(cmd, *args, **kwargs)
452 p = self._run(cmd, *args, **kwargs)
453 output = p.stdout.readlines()
453 output = p.stdout.readlines()
454 p.wait()
454 p.wait()
455 self.ui.debug(b''.join(output))
455 self.ui.debug(b''.join(output))
456 return output, p.returncode
456 return output, p.returncode
457
457
458 def checkexit(self, status, output=b''):
458 def checkexit(self, status, output=b''):
459 if status:
459 if status:
460 if output:
460 if output:
461 self.ui.warn(_(b'%s error:\n') % self.command)
461 self.ui.warn(_(b'%s error:\n') % self.command)
462 self.ui.warn(output)
462 self.ui.warn(output)
463 msg = procutil.explainexit(status)
463 msg = procutil.explainexit(status)
464 raise error.Abort(b'%s %s' % (self.command, msg))
464 raise error.Abort(b'%s %s' % (self.command, msg))
465
465
466 def run0(self, cmd, *args, **kwargs):
466 def run0(self, cmd, *args, **kwargs):
467 output, status = self.run(cmd, *args, **kwargs)
467 output, status = self.run(cmd, *args, **kwargs)
468 self.checkexit(status, output)
468 self.checkexit(status, output)
469 return output
469 return output
470
470
471 def runlines0(self, cmd, *args, **kwargs):
471 def runlines0(self, cmd, *args, **kwargs):
472 output, status = self.runlines(cmd, *args, **kwargs)
472 output, status = self.runlines(cmd, *args, **kwargs)
473 self.checkexit(status, b''.join(output))
473 self.checkexit(status, b''.join(output))
474 return output
474 return output
475
475
476 @propertycache
476 @propertycache
477 def argmax(self):
477 def argmax(self):
478 # POSIX requires at least 4096 bytes for ARG_MAX
478 # POSIX requires at least 4096 bytes for ARG_MAX
479 argmax = 4096
479 argmax = 4096
480 try:
480 try:
481 argmax = os.sysconf("SC_ARG_MAX")
481 argmax = os.sysconf("SC_ARG_MAX")
482 except (AttributeError, ValueError):
482 except (AttributeError, ValueError):
483 pass
483 pass
484
484
485 # Windows shells impose their own limits on command line length,
485 # Windows shells impose their own limits on command line length,
486 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
486 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
487 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
487 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
488 # details about cmd.exe limitations.
488 # details about cmd.exe limitations.
489
489
490 # Since ARG_MAX is for command line _and_ environment, lower our limit
490 # Since ARG_MAX is for command line _and_ environment, lower our limit
491 # (and make happy Windows shells while doing this).
491 # (and make happy Windows shells while doing this).
492 return argmax // 2 - 1
492 return argmax // 2 - 1
493
493
494 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
494 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
495 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
495 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
496 limit = self.argmax - cmdlen
496 limit = self.argmax - cmdlen
497 numbytes = 0
497 numbytes = 0
498 fl = []
498 fl = []
499 for fn in arglist:
499 for fn in arglist:
500 b = len(fn) + 3
500 b = len(fn) + 3
501 if numbytes + b < limit or len(fl) == 0:
501 if numbytes + b < limit or len(fl) == 0:
502 fl.append(fn)
502 fl.append(fn)
503 numbytes += b
503 numbytes += b
504 else:
504 else:
505 yield fl
505 yield fl
506 fl = [fn]
506 fl = [fn]
507 numbytes = b
507 numbytes = b
508 if fl:
508 if fl:
509 yield fl
509 yield fl
510
510
511 def xargs(self, arglist, cmd, *args, **kwargs):
511 def xargs(self, arglist, cmd, *args, **kwargs):
512 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
512 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
513 self.run0(cmd, *(list(args) + l), **kwargs)
513 self.run0(cmd, *(list(args) + l), **kwargs)
514
514
515
515
516 class mapfile(dict):
516 class mapfile(dict):
517 def __init__(self, ui, path):
517 def __init__(self, ui, path):
518 super(mapfile, self).__init__()
518 super(mapfile, self).__init__()
519 self.ui = ui
519 self.ui = ui
520 self.path = path
520 self.path = path
521 self.fp = None
521 self.fp = None
522 self.order = []
522 self.order = []
523 self._read()
523 self._read()
524
524
525 def _read(self):
525 def _read(self):
526 if not self.path:
526 if not self.path:
527 return
527 return
528 try:
528 try:
529 fp = open(self.path, b'rb')
529 fp = open(self.path, b'rb')
530 except FileNotFoundError:
530 except FileNotFoundError:
531 return
531 return
532 for i, line in enumerate(fp):
532 for i, line in enumerate(fp):
533 line = line.splitlines()[0].rstrip()
533 line = line.splitlines()[0].rstrip()
534 if not line:
534 if not line:
535 # Ignore blank lines
535 # Ignore blank lines
536 continue
536 continue
537 try:
537 try:
538 key, value = line.rsplit(b' ', 1)
538 key, value = line.rsplit(b' ', 1)
539 except ValueError:
539 except ValueError:
540 raise error.Abort(
540 raise error.Abort(
541 _(b'syntax error in %s(%d): key/value pair expected')
541 _(b'syntax error in %s(%d): key/value pair expected')
542 % (self.path, i + 1)
542 % (self.path, i + 1)
543 )
543 )
544 if key not in self:
544 if key not in self:
545 self.order.append(key)
545 self.order.append(key)
546 super(mapfile, self).__setitem__(key, value)
546 super(mapfile, self).__setitem__(key, value)
547 fp.close()
547 fp.close()
548
548
549 def __setitem__(self, key, value):
549 def __setitem__(self, key, value):
550 if self.fp is None:
550 if self.fp is None:
551 try:
551 try:
552 self.fp = open(self.path, b'ab')
552 self.fp = open(self.path, b'ab')
553 except IOError as err:
553 except IOError as err:
554 raise error.Abort(
554 raise error.Abort(
555 _(b'could not open map file %r: %s')
555 _(b'could not open map file %r: %s')
556 % (self.path, encoding.strtolocal(err.strerror))
556 % (self.path, encoding.strtolocal(err.strerror))
557 )
557 )
558 self.fp.write(util.tonativeeol(b'%s %s\n' % (key, value)))
558 self.fp.write(util.tonativeeol(b'%s %s\n' % (key, value)))
559 self.fp.flush()
559 self.fp.flush()
560 super(mapfile, self).__setitem__(key, value)
560 super(mapfile, self).__setitem__(key, value)
561
561
562 def close(self):
562 def close(self):
563 if self.fp:
563 if self.fp:
564 self.fp.close()
564 self.fp.close()
565 self.fp = None
565 self.fp = None
566
566
567
567
568 def makedatetimestamp(t):
568 def makedatetimestamp(t):
569 """Like dateutil.makedate() but for time t instead of current time"""
569 """Like dateutil.makedate() but for time t instead of current time"""
570 delta = datetime.datetime.utcfromtimestamp(
570 tz = round(
571 t
571 t
572 ) - datetime.datetime.fromtimestamp(t)
572 - datetime.datetime.fromtimestamp(t)
573 tz = delta.days * 86400 + delta.seconds
573 .replace(tzinfo=datetime.timezone.utc)
574 .timestamp()
575 )
574 return t, tz
576 return t, tz
@@ -1,386 +1,390 b''
1 # util.py - Mercurial utility functions relative to dates
1 # util.py - Mercurial utility functions relative to dates
2 #
2 #
3 # Copyright 2018 Boris Feld <boris.feld@octobus.net>
3 # Copyright 2018 Boris Feld <boris.feld@octobus.net>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8
8
9 import calendar
9 import calendar
10 import datetime
10 import datetime
11 import time
11 import time
12
12
13 from ..i18n import _
13 from ..i18n import _
14 from .. import (
14 from .. import (
15 encoding,
15 encoding,
16 error,
16 error,
17 pycompat,
17 pycompat,
18 )
18 )
19
19
20 if pycompat.TYPE_CHECKING:
20 if pycompat.TYPE_CHECKING:
21 from typing import (
21 from typing import (
22 Callable,
22 Callable,
23 Dict,
23 Dict,
24 Iterable,
24 Iterable,
25 Optional,
25 Optional,
26 Tuple,
26 Tuple,
27 Union,
27 Union,
28 )
28 )
29
29
30 hgdate = Tuple[float, int] # (unixtime, offset)
30 hgdate = Tuple[float, int] # (unixtime, offset)
31
31
32 # used by parsedate
32 # used by parsedate
33 defaultdateformats = (
33 defaultdateformats = (
34 b'%Y-%m-%dT%H:%M:%S', # the 'real' ISO8601
34 b'%Y-%m-%dT%H:%M:%S', # the 'real' ISO8601
35 b'%Y-%m-%dT%H:%M', # without seconds
35 b'%Y-%m-%dT%H:%M', # without seconds
36 b'%Y-%m-%dT%H%M%S', # another awful but legal variant without :
36 b'%Y-%m-%dT%H%M%S', # another awful but legal variant without :
37 b'%Y-%m-%dT%H%M', # without seconds
37 b'%Y-%m-%dT%H%M', # without seconds
38 b'%Y-%m-%d %H:%M:%S', # our common legal variant
38 b'%Y-%m-%d %H:%M:%S', # our common legal variant
39 b'%Y-%m-%d %H:%M', # without seconds
39 b'%Y-%m-%d %H:%M', # without seconds
40 b'%Y-%m-%d %H%M%S', # without :
40 b'%Y-%m-%d %H%M%S', # without :
41 b'%Y-%m-%d %H%M', # without seconds
41 b'%Y-%m-%d %H%M', # without seconds
42 b'%Y-%m-%d %I:%M:%S%p',
42 b'%Y-%m-%d %I:%M:%S%p',
43 b'%Y-%m-%d %H:%M',
43 b'%Y-%m-%d %H:%M',
44 b'%Y-%m-%d %I:%M%p',
44 b'%Y-%m-%d %I:%M%p',
45 b'%Y-%m-%d',
45 b'%Y-%m-%d',
46 b'%m-%d',
46 b'%m-%d',
47 b'%m/%d',
47 b'%m/%d',
48 b'%m/%d/%y',
48 b'%m/%d/%y',
49 b'%m/%d/%Y',
49 b'%m/%d/%Y',
50 b'%a %b %d %H:%M:%S %Y',
50 b'%a %b %d %H:%M:%S %Y',
51 b'%a %b %d %I:%M:%S%p %Y',
51 b'%a %b %d %I:%M:%S%p %Y',
52 b'%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
52 b'%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
53 b'%b %d %H:%M:%S %Y',
53 b'%b %d %H:%M:%S %Y',
54 b'%b %d %I:%M:%S%p %Y',
54 b'%b %d %I:%M:%S%p %Y',
55 b'%b %d %H:%M:%S',
55 b'%b %d %H:%M:%S',
56 b'%b %d %I:%M:%S%p',
56 b'%b %d %I:%M:%S%p',
57 b'%b %d %H:%M',
57 b'%b %d %H:%M',
58 b'%b %d %I:%M%p',
58 b'%b %d %I:%M%p',
59 b'%b %d %Y',
59 b'%b %d %Y',
60 b'%b %d',
60 b'%b %d',
61 b'%H:%M:%S',
61 b'%H:%M:%S',
62 b'%I:%M:%S%p',
62 b'%I:%M:%S%p',
63 b'%H:%M',
63 b'%H:%M',
64 b'%I:%M%p',
64 b'%I:%M%p',
65 )
65 )
66
66
67 extendeddateformats = defaultdateformats + (
67 extendeddateformats = defaultdateformats + (
68 b"%Y",
68 b"%Y",
69 b"%Y-%m",
69 b"%Y-%m",
70 b"%b",
70 b"%b",
71 b"%b %Y",
71 b"%b %Y",
72 )
72 )
73
73
74
74
75 def makedate(timestamp=None):
75 def makedate(timestamp=None):
76 # type: (Optional[float]) -> hgdate
76 # type: (Optional[float]) -> hgdate
77 """Return a unix timestamp (or the current time) as a (unixtime,
77 """Return a unix timestamp (or the current time) as a (unixtime,
78 offset) tuple based off the local timezone."""
78 offset) tuple based off the local timezone."""
79 if timestamp is None:
79 if timestamp is None:
80 timestamp = time.time()
80 timestamp = time.time()
81 if timestamp < 0:
81 if timestamp < 0:
82 hint = _(b"check your clock")
82 hint = _(b"check your clock")
83 raise error.InputError(
83 raise error.InputError(
84 _(b"negative timestamp: %d") % timestamp, hint=hint
84 _(b"negative timestamp: %d") % timestamp, hint=hint
85 )
85 )
86 delta = datetime.datetime.utcfromtimestamp(
86 tz = round(
87 timestamp
87 timestamp
88 ) - datetime.datetime.fromtimestamp(timestamp)
88 - datetime.datetime.fromtimestamp(
89 tz = delta.days * 86400 + delta.seconds
89 timestamp,
90 )
91 .replace(tzinfo=datetime.timezone.utc)
92 .timestamp()
93 )
90 return timestamp, tz
94 return timestamp, tz
91
95
92
96
93 def datestr(date=None, format=b'%a %b %d %H:%M:%S %Y %1%2'):
97 def datestr(date=None, format=b'%a %b %d %H:%M:%S %Y %1%2'):
94 # type: (Optional[hgdate], bytes) -> bytes
98 # type: (Optional[hgdate], bytes) -> bytes
95 """represent a (unixtime, offset) tuple as a localized time.
99 """represent a (unixtime, offset) tuple as a localized time.
96 unixtime is seconds since the epoch, and offset is the time zone's
100 unixtime is seconds since the epoch, and offset is the time zone's
97 number of seconds away from UTC.
101 number of seconds away from UTC.
98
102
99 >>> datestr((0, 0))
103 >>> datestr((0, 0))
100 'Thu Jan 01 00:00:00 1970 +0000'
104 'Thu Jan 01 00:00:00 1970 +0000'
101 >>> datestr((42, 0))
105 >>> datestr((42, 0))
102 'Thu Jan 01 00:00:42 1970 +0000'
106 'Thu Jan 01 00:00:42 1970 +0000'
103 >>> datestr((-42, 0))
107 >>> datestr((-42, 0))
104 'Wed Dec 31 23:59:18 1969 +0000'
108 'Wed Dec 31 23:59:18 1969 +0000'
105 >>> datestr((0x7fffffff, 0))
109 >>> datestr((0x7fffffff, 0))
106 'Tue Jan 19 03:14:07 2038 +0000'
110 'Tue Jan 19 03:14:07 2038 +0000'
107 >>> datestr((-0x80000000, 0))
111 >>> datestr((-0x80000000, 0))
108 'Fri Dec 13 20:45:52 1901 +0000'
112 'Fri Dec 13 20:45:52 1901 +0000'
109 """
113 """
110 t, tz = date or makedate()
114 t, tz = date or makedate()
111 if b"%1" in format or b"%2" in format or b"%z" in format:
115 if b"%1" in format or b"%2" in format or b"%z" in format:
112 sign = (tz > 0) and b"-" or b"+"
116 sign = (tz > 0) and b"-" or b"+"
113 minutes = abs(tz) // 60
117 minutes = abs(tz) // 60
114 q, r = divmod(minutes, 60)
118 q, r = divmod(minutes, 60)
115 format = format.replace(b"%z", b"%1%2")
119 format = format.replace(b"%z", b"%1%2")
116 format = format.replace(b"%1", b"%c%02d" % (sign, q))
120 format = format.replace(b"%1", b"%c%02d" % (sign, q))
117 format = format.replace(b"%2", b"%02d" % r)
121 format = format.replace(b"%2", b"%02d" % r)
118 d = t - tz
122 d = t - tz
119 if d > 0x7FFFFFFF:
123 if d > 0x7FFFFFFF:
120 d = 0x7FFFFFFF
124 d = 0x7FFFFFFF
121 elif d < -0x80000000:
125 elif d < -0x80000000:
122 d = -0x80000000
126 d = -0x80000000
123 # Never use time.gmtime() and datetime.datetime.fromtimestamp()
127 # Never use time.gmtime() and datetime.datetime.fromtimestamp()
124 # because they use the gmtime() system call which is buggy on Windows
128 # because they use the gmtime() system call which is buggy on Windows
125 # for negative values.
129 # for negative values.
126 t = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=d)
130 t = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=d)
127 s = encoding.strtolocal(t.strftime(encoding.strfromlocal(format)))
131 s = encoding.strtolocal(t.strftime(encoding.strfromlocal(format)))
128 return s
132 return s
129
133
130
134
131 def shortdate(date=None):
135 def shortdate(date=None):
132 # type: (Optional[hgdate]) -> bytes
136 # type: (Optional[hgdate]) -> bytes
133 """turn (timestamp, tzoff) tuple into iso 8631 date."""
137 """turn (timestamp, tzoff) tuple into iso 8631 date."""
134 return datestr(date, format=b'%Y-%m-%d')
138 return datestr(date, format=b'%Y-%m-%d')
135
139
136
140
137 def parsetimezone(s):
141 def parsetimezone(s):
138 # type: (bytes) -> Tuple[Optional[int], bytes]
142 # type: (bytes) -> Tuple[Optional[int], bytes]
139 """find a trailing timezone, if any, in string, and return a
143 """find a trailing timezone, if any, in string, and return a
140 (offset, remainder) pair"""
144 (offset, remainder) pair"""
141 s = pycompat.bytestr(s)
145 s = pycompat.bytestr(s)
142
146
143 if s.endswith(b"GMT") or s.endswith(b"UTC"):
147 if s.endswith(b"GMT") or s.endswith(b"UTC"):
144 return 0, s[:-3].rstrip()
148 return 0, s[:-3].rstrip()
145
149
146 # Unix-style timezones [+-]hhmm
150 # Unix-style timezones [+-]hhmm
147 if len(s) >= 5 and s[-5] in b"+-" and s[-4:].isdigit():
151 if len(s) >= 5 and s[-5] in b"+-" and s[-4:].isdigit():
148 sign = (s[-5] == b"+") and 1 or -1
152 sign = (s[-5] == b"+") and 1 or -1
149 hours = int(s[-4:-2])
153 hours = int(s[-4:-2])
150 minutes = int(s[-2:])
154 minutes = int(s[-2:])
151 return -sign * (hours * 60 + minutes) * 60, s[:-5].rstrip()
155 return -sign * (hours * 60 + minutes) * 60, s[:-5].rstrip()
152
156
153 # ISO8601 trailing Z
157 # ISO8601 trailing Z
154 if s.endswith(b"Z") and s[-2:-1].isdigit():
158 if s.endswith(b"Z") and s[-2:-1].isdigit():
155 return 0, s[:-1]
159 return 0, s[:-1]
156
160
157 # ISO8601-style [+-]hh:mm
161 # ISO8601-style [+-]hh:mm
158 if (
162 if (
159 len(s) >= 6
163 len(s) >= 6
160 and s[-6] in b"+-"
164 and s[-6] in b"+-"
161 and s[-3] == b":"
165 and s[-3] == b":"
162 and s[-5:-3].isdigit()
166 and s[-5:-3].isdigit()
163 and s[-2:].isdigit()
167 and s[-2:].isdigit()
164 ):
168 ):
165 sign = (s[-6] == b"+") and 1 or -1
169 sign = (s[-6] == b"+") and 1 or -1
166 hours = int(s[-5:-3])
170 hours = int(s[-5:-3])
167 minutes = int(s[-2:])
171 minutes = int(s[-2:])
168 return -sign * (hours * 60 + minutes) * 60, s[:-6]
172 return -sign * (hours * 60 + minutes) * 60, s[:-6]
169
173
170 return None, s
174 return None, s
171
175
172
176
173 def strdate(string, format, defaults=None):
177 def strdate(string, format, defaults=None):
174 # type: (bytes, bytes, Optional[Dict[bytes, Tuple[bytes, bytes]]]) -> hgdate
178 # type: (bytes, bytes, Optional[Dict[bytes, Tuple[bytes, bytes]]]) -> hgdate
175 """parse a localized time string and return a (unixtime, offset) tuple.
179 """parse a localized time string and return a (unixtime, offset) tuple.
176 if the string cannot be parsed, ValueError is raised."""
180 if the string cannot be parsed, ValueError is raised."""
177 if defaults is None:
181 if defaults is None:
178 defaults = {}
182 defaults = {}
179
183
180 # NOTE: unixtime = localunixtime + offset
184 # NOTE: unixtime = localunixtime + offset
181 offset, date = parsetimezone(string)
185 offset, date = parsetimezone(string)
182
186
183 # add missing elements from defaults
187 # add missing elements from defaults
184 usenow = False # default to using biased defaults
188 usenow = False # default to using biased defaults
185 for part in (
189 for part in (
186 b"S",
190 b"S",
187 b"M",
191 b"M",
188 b"HI",
192 b"HI",
189 b"d",
193 b"d",
190 b"mb",
194 b"mb",
191 b"yY",
195 b"yY",
192 ): # decreasing specificity
196 ): # decreasing specificity
193 part = pycompat.bytestr(part)
197 part = pycompat.bytestr(part)
194 found = [True for p in part if (b"%" + p) in format]
198 found = [True for p in part if (b"%" + p) in format]
195 if not found:
199 if not found:
196 date += b"@" + defaults[part][usenow]
200 date += b"@" + defaults[part][usenow]
197 format += b"@%" + part[0]
201 format += b"@%" + part[0]
198 else:
202 else:
199 # We've found a specific time element, less specific time
203 # We've found a specific time element, less specific time
200 # elements are relative to today
204 # elements are relative to today
201 usenow = True
205 usenow = True
202
206
203 timetuple = time.strptime(
207 timetuple = time.strptime(
204 encoding.strfromlocal(date), encoding.strfromlocal(format)
208 encoding.strfromlocal(date), encoding.strfromlocal(format)
205 )
209 )
206 localunixtime = int(calendar.timegm(timetuple))
210 localunixtime = int(calendar.timegm(timetuple))
207 if offset is None:
211 if offset is None:
208 # local timezone
212 # local timezone
209 unixtime = int(time.mktime(timetuple))
213 unixtime = int(time.mktime(timetuple))
210 offset = unixtime - localunixtime
214 offset = unixtime - localunixtime
211 else:
215 else:
212 unixtime = localunixtime + offset
216 unixtime = localunixtime + offset
213 return unixtime, offset
217 return unixtime, offset
214
218
215
219
216 def parsedate(date, formats=None, bias=None):
220 def parsedate(date, formats=None, bias=None):
217 # type: (Union[bytes, hgdate], Optional[Iterable[bytes]], Optional[Dict[bytes, bytes]]) -> hgdate
221 # type: (Union[bytes, hgdate], Optional[Iterable[bytes]], Optional[Dict[bytes, bytes]]) -> hgdate
218 """parse a localized date/time and return a (unixtime, offset) tuple.
222 """parse a localized date/time and return a (unixtime, offset) tuple.
219
223
220 The date may be a "unixtime offset" string or in one of the specified
224 The date may be a "unixtime offset" string or in one of the specified
221 formats. If the date already is a (unixtime, offset) tuple, it is returned.
225 formats. If the date already is a (unixtime, offset) tuple, it is returned.
222
226
223 >>> parsedate(b' today ') == parsedate(
227 >>> parsedate(b' today ') == parsedate(
224 ... datetime.date.today().strftime('%b %d').encode('ascii'))
228 ... datetime.date.today().strftime('%b %d').encode('ascii'))
225 True
229 True
226 >>> parsedate(b'yesterday ') == parsedate(
230 >>> parsedate(b'yesterday ') == parsedate(
227 ... (datetime.date.today() - datetime.timedelta(days=1)
231 ... (datetime.date.today() - datetime.timedelta(days=1)
228 ... ).strftime('%b %d').encode('ascii'))
232 ... ).strftime('%b %d').encode('ascii'))
229 True
233 True
230 >>> now, tz = makedate()
234 >>> now, tz = makedate()
231 >>> strnow, strtz = parsedate(b'now')
235 >>> strnow, strtz = parsedate(b'now')
232 >>> (strnow - now) < 1
236 >>> (strnow - now) < 1
233 True
237 True
234 >>> tz == strtz
238 >>> tz == strtz
235 True
239 True
236 >>> parsedate(b'2000 UTC', formats=extendeddateformats)
240 >>> parsedate(b'2000 UTC', formats=extendeddateformats)
237 (946684800, 0)
241 (946684800, 0)
238 """
242 """
239 if bias is None:
243 if bias is None:
240 bias = {}
244 bias = {}
241 if not date:
245 if not date:
242 return 0, 0
246 return 0, 0
243 if isinstance(date, tuple):
247 if isinstance(date, tuple):
244 if len(date) == 2:
248 if len(date) == 2:
245 return date
249 return date
246 else:
250 else:
247 raise error.ProgrammingError(b"invalid date format")
251 raise error.ProgrammingError(b"invalid date format")
248 if not formats:
252 if not formats:
249 formats = defaultdateformats
253 formats = defaultdateformats
250 date = date.strip()
254 date = date.strip()
251
255
252 if date == b'now' or date == _(b'now'):
256 if date == b'now' or date == _(b'now'):
253 return makedate()
257 return makedate()
254 if date == b'today' or date == _(b'today'):
258 if date == b'today' or date == _(b'today'):
255 date = datetime.date.today().strftime('%b %d')
259 date = datetime.date.today().strftime('%b %d')
256 date = encoding.strtolocal(date)
260 date = encoding.strtolocal(date)
257 elif date == b'yesterday' or date == _(b'yesterday'):
261 elif date == b'yesterday' or date == _(b'yesterday'):
258 date = (datetime.date.today() - datetime.timedelta(days=1)).strftime(
262 date = (datetime.date.today() - datetime.timedelta(days=1)).strftime(
259 r'%b %d'
263 r'%b %d'
260 )
264 )
261 date = encoding.strtolocal(date)
265 date = encoding.strtolocal(date)
262
266
263 try:
267 try:
264 when, offset = map(int, date.split(b' '))
268 when, offset = map(int, date.split(b' '))
265 except ValueError:
269 except ValueError:
266 # fill out defaults
270 # fill out defaults
267 now = makedate()
271 now = makedate()
268 defaults = {}
272 defaults = {}
269 for part in (b"d", b"mb", b"yY", b"HI", b"M", b"S"):
273 for part in (b"d", b"mb", b"yY", b"HI", b"M", b"S"):
270 # this piece is for rounding the specific end of unknowns
274 # this piece is for rounding the specific end of unknowns
271 b = bias.get(part)
275 b = bias.get(part)
272 if b is None:
276 if b is None:
273 if part[0:1] in b"HMS":
277 if part[0:1] in b"HMS":
274 b = b"00"
278 b = b"00"
275 else:
279 else:
276 # year, month, and day start from 1
280 # year, month, and day start from 1
277 b = b"1"
281 b = b"1"
278
282
279 # this piece is for matching the generic end to today's date
283 # this piece is for matching the generic end to today's date
280 n = datestr(now, b"%" + part[0:1])
284 n = datestr(now, b"%" + part[0:1])
281
285
282 defaults[part] = (b, n)
286 defaults[part] = (b, n)
283
287
284 for format in formats:
288 for format in formats:
285 try:
289 try:
286 when, offset = strdate(date, format, defaults)
290 when, offset = strdate(date, format, defaults)
287 except (ValueError, OverflowError):
291 except (ValueError, OverflowError):
288 pass
292 pass
289 else:
293 else:
290 break
294 break
291 else:
295 else:
292 raise error.ParseError(
296 raise error.ParseError(
293 _(b'invalid date: %r') % pycompat.bytestr(date)
297 _(b'invalid date: %r') % pycompat.bytestr(date)
294 )
298 )
295 # validate explicit (probably user-specified) date and
299 # validate explicit (probably user-specified) date and
296 # time zone offset. values must fit in signed 32 bits for
300 # time zone offset. values must fit in signed 32 bits for
297 # current 32-bit linux runtimes. timezones go from UTC-12
301 # current 32-bit linux runtimes. timezones go from UTC-12
298 # to UTC+14
302 # to UTC+14
299 if when < -0x80000000 or when > 0x7FFFFFFF:
303 if when < -0x80000000 or when > 0x7FFFFFFF:
300 raise error.ParseError(_(b'date exceeds 32 bits: %d') % when)
304 raise error.ParseError(_(b'date exceeds 32 bits: %d') % when)
301 if offset < -50400 or offset > 43200:
305 if offset < -50400 or offset > 43200:
302 raise error.ParseError(_(b'impossible time zone offset: %d') % offset)
306 raise error.ParseError(_(b'impossible time zone offset: %d') % offset)
303 return when, offset
307 return when, offset
304
308
305
309
306 def matchdate(date):
310 def matchdate(date):
307 # type: (bytes) -> Callable[[float], bool]
311 # type: (bytes) -> Callable[[float], bool]
308 """Return a function that matches a given date match specifier
312 """Return a function that matches a given date match specifier
309
313
310 Formats include:
314 Formats include:
311
315
312 '{date}' match a given date to the accuracy provided
316 '{date}' match a given date to the accuracy provided
313
317
314 '<{date}' on or before a given date
318 '<{date}' on or before a given date
315
319
316 '>{date}' on or after a given date
320 '>{date}' on or after a given date
317
321
318 >>> p1 = parsedate(b"10:29:59")
322 >>> p1 = parsedate(b"10:29:59")
319 >>> p2 = parsedate(b"10:30:00")
323 >>> p2 = parsedate(b"10:30:00")
320 >>> p3 = parsedate(b"10:30:59")
324 >>> p3 = parsedate(b"10:30:59")
321 >>> p4 = parsedate(b"10:31:00")
325 >>> p4 = parsedate(b"10:31:00")
322 >>> p5 = parsedate(b"Sep 15 10:30:00 1999")
326 >>> p5 = parsedate(b"Sep 15 10:30:00 1999")
323 >>> f = matchdate(b"10:30")
327 >>> f = matchdate(b"10:30")
324 >>> f(p1[0])
328 >>> f(p1[0])
325 False
329 False
326 >>> f(p2[0])
330 >>> f(p2[0])
327 True
331 True
328 >>> f(p3[0])
332 >>> f(p3[0])
329 True
333 True
330 >>> f(p4[0])
334 >>> f(p4[0])
331 False
335 False
332 >>> f(p5[0])
336 >>> f(p5[0])
333 False
337 False
334 """
338 """
335
339
336 def lower(date):
340 def lower(date):
337 # type: (bytes) -> float
341 # type: (bytes) -> float
338 d = {b'mb': b"1", b'd': b"1"}
342 d = {b'mb': b"1", b'd': b"1"}
339 return parsedate(date, extendeddateformats, d)[0]
343 return parsedate(date, extendeddateformats, d)[0]
340
344
341 def upper(date):
345 def upper(date):
342 # type: (bytes) -> float
346 # type: (bytes) -> float
343 d = {b'mb': b"12", b'HI': b"23", b'M': b"59", b'S': b"59"}
347 d = {b'mb': b"12", b'HI': b"23", b'M': b"59", b'S': b"59"}
344 for days in (b"31", b"30", b"29"):
348 for days in (b"31", b"30", b"29"):
345 try:
349 try:
346 d[b"d"] = days
350 d[b"d"] = days
347 return parsedate(date, extendeddateformats, d)[0]
351 return parsedate(date, extendeddateformats, d)[0]
348 except error.ParseError:
352 except error.ParseError:
349 pass
353 pass
350 d[b"d"] = b"28"
354 d[b"d"] = b"28"
351 return parsedate(date, extendeddateformats, d)[0]
355 return parsedate(date, extendeddateformats, d)[0]
352
356
353 date = date.strip()
357 date = date.strip()
354
358
355 if not date:
359 if not date:
356 raise error.InputError(
360 raise error.InputError(
357 _(b"dates cannot consist entirely of whitespace")
361 _(b"dates cannot consist entirely of whitespace")
358 )
362 )
359 elif date[0:1] == b"<":
363 elif date[0:1] == b"<":
360 if not date[1:]:
364 if not date[1:]:
361 raise error.InputError(_(b"invalid day spec, use '<DATE'"))
365 raise error.InputError(_(b"invalid day spec, use '<DATE'"))
362 when = upper(date[1:])
366 when = upper(date[1:])
363 return lambda x: x <= when
367 return lambda x: x <= when
364 elif date[0:1] == b">":
368 elif date[0:1] == b">":
365 if not date[1:]:
369 if not date[1:]:
366 raise error.InputError(_(b"invalid day spec, use '>DATE'"))
370 raise error.InputError(_(b"invalid day spec, use '>DATE'"))
367 when = lower(date[1:])
371 when = lower(date[1:])
368 return lambda x: x >= when
372 return lambda x: x >= when
369 elif date[0:1] == b"-":
373 elif date[0:1] == b"-":
370 try:
374 try:
371 days = int(date[1:])
375 days = int(date[1:])
372 except ValueError:
376 except ValueError:
373 raise error.InputError(_(b"invalid day spec: %s") % date[1:])
377 raise error.InputError(_(b"invalid day spec: %s") % date[1:])
374 if days < 0:
378 if days < 0:
375 raise error.InputError(
379 raise error.InputError(
376 _(b"%s must be nonnegative (see 'hg help dates')") % date[1:]
380 _(b"%s must be nonnegative (see 'hg help dates')") % date[1:]
377 )
381 )
378 when = makedate()[0] - days * 3600 * 24
382 when = makedate()[0] - days * 3600 * 24
379 return lambda x: x >= when
383 return lambda x: x >= when
380 elif b" to " in date:
384 elif b" to " in date:
381 a, b = date.split(b" to ")
385 a, b = date.split(b" to ")
382 start, stop = lower(a), upper(b)
386 start, stop = lower(a), upper(b)
383 return lambda x: x >= start and x <= stop
387 return lambda x: x >= start and x <= stop
384 else:
388 else:
385 start, stop = lower(date), upper(date)
389 start, stop = lower(date), upper(date)
386 return lambda x: x >= start and x <= stop
390 return lambda x: x >= start and x <= stop
General Comments 0
You need to be logged in to leave comments. Login now