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