Commit Graph

377 Commits

Author SHA1 Message Date
Jun Wu
f40e980428 pypager: rename to pyio
Summary:
I'm going to expose more APIs from the Rust IO such as setting progress
content, etc. Rename the module to clarify.

Reviewed By: sfilipco

Differential Revision: D26612490

fbshipit-source-id: 136ea27a733b09557d02077e68ce51184125ade1
2021-02-23 22:33:46 -08:00
Jun Wu
b3a8cd238e progress: rename write_progress to set_progress
Summary:
Unlike stderr or stdout, writing to progress clears the previous progress text.
Update the function name to clarify.

Reviewed By: sfilipco

Differential Revision: D26612481

fbshipit-source-id: 382182fdcc713f92117d01f53b8da47d195b06cb
2021-02-23 22:33:46 -08:00
Jun Wu
fe8dada0e6 progress: change write_progress to take &str
Summary:
There seems no need for the progress bar text to be binary (it is not used to
redirect to binary files). Let's change the type to make future changes easier.

Reviewed By: sfilipco

Differential Revision: D26612479

fbshipit-source-id: 1da7950d5ab2b590963f1591e2f1dd5ae8e0116c
2021-02-23 22:33:45 -08:00
Andrey Chursin
3c1884034f checkout: use allow_threads in native checkout
Summary: This allow python to redraw progress bar

Reviewed By: quark-zju

Differential Revision: D26613054

fbshipit-source-id: 717f2c15fd9f9ccf339b457529c4f070d1f7ab79
2021-02-23 15:45:04 -08:00
Jun Wu
e46d5c4295 clidispatch: drop unused mut from IO APIs
Summary: Now IO has internal locking, `mut` is no longer needed.

Reviewed By: sfilipco

Differential Revision: D26518025

fbshipit-source-id: d71e213f6fed24fdefb4c730d576f6cf2fb09d82
2021-02-23 15:09:14 -08:00
Jun Wu
57adadaf1d pypager: use the global IO state
Summary:
Use the global IO state instead of keeping a separate IO state.

This makes the Python pager logic able to affect the tracing logger's error
output. For example, the following command will write the tracing logs to
the streampager's stderr channel as expected:

    EDENSCM_LOG=trace lhg log -r '.~1000::.' --config pager.pager=internal:streampager

Traditional pager also works:

    EDENSCM_LOG=trace lhg log -r '.~1000::.' --config pager.pager=less

Reviewed By: sfilipco

Differential Revision: D26518024

fbshipit-source-id: c7020b7467f9392af8c2a58d064339d05bce35ce
2021-02-23 15:09:13 -08:00
Andrey Chursin
adb28ae0aa checkout: expose stats to Python
Reviewed By: quark-zju

Differential Revision: D26552742

fbshipit-source-id: fccc812eb1f3ad150580538a1d8fa4e8afd3bd7b
2021-02-20 01:29:10 -08:00
Andrey Chursin
2646ed375b checkout: record updates after checkout
Summary: This is needed to correctly show status after checkout

Reviewed By: quark-zju

Differential Revision: D26551119

fbshipit-source-id: 3ab576df8132b1fb8cccf507717923ecf3084757
2021-02-20 01:29:09 -08:00
Andrey Chursin
c7269df47e checkout: impl Display for CheckoutPlan
Summary: Also add __str__ Python binding. Usefull for debugging

Reviewed By: quark-zju

Differential Revision: D26496791

fbshipit-source-id: 8bcfc89918c5fafc9ac8e42ef91c576b160d943c
2021-02-19 10:30:01 -08:00
Andrey Chursin
ceee768281 pycheckout: register checkout module in populate_module
Reviewed By: DurhamG

Differential Revision: D26470216

fbshipit-source-id: cdfda074256ba7dbf90d807904bab9a3b8142a98
2021-02-17 12:02:44 -08:00
Andrey Chursin
6498f4f892 pycheckout: add checkoutplan::apply
Reviewed By: DurhamG

Differential Revision: D26469704

fbshipit-source-id: 010a12b04b7f6a5d08e1aa248130026341306595
2021-02-17 12:02:43 -08:00
Andrey Chursin
51a29c5d7e pycheckout: implement checkoutplan::__new__
Summary: This diff introduces a Python checkoutplan, which will be used later for Python code to interact with the Rust checkout infrastructure

Reviewed By: DurhamG

Differential Revision: D26437080

fbshipit-source-id: fe3517f4e2f15244a7c2cd290a41394e904db51b
2021-02-17 12:02:43 -08:00
Andrey Chursin
6ae7c785ba pymanifest: make treemanifest public
Summary: This diff exposes treemanifest for public use and introduce method to borrow underlyning manifest for usage in rust

Reviewed By: DurhamG

Differential Revision: D26437079

fbshipit-source-id: d2d6cd2a4d1f108881d985932a36d48fed4b238d
2021-02-17 12:02:43 -08:00
Andrey Chursin
11faa47a7d pycheckout: empty pycheckout crate
Reviewed By: DurhamG

Differential Revision: D26435596

fbshipit-source-id: 013070ff918c7b45105c7abdad0950ee96d46d49
2021-02-17 12:02:43 -08:00
Arun Kulshreshtha
c97db6b042 auth: rename structs
Summary:
Make the struct and method names in this crate more clearly reflective of what they do:

- `Auth` -> `AuthGroup`
- `Auth::try_from` -> `AuthGroup::new`
- `AuthConfig` -> `AuthSection`
- `AuthConfig::new` -> `AuthSection::from_config`
- `AuthConfig::auth_for_url` -> `AuthSection::best_match_for`

Reviewed By: singhsrb

Differential Revision: D26436095

fbshipit-source-id: a5ec5d9c48d3b75a0ee166b74e5340f9c529eeae
2021-02-12 17:52:29 -08:00
Zeyi (Rice) Fan
d552144478 configparser: move conversion related to a separated module
Summary:
Move these conversion related function and trait out of `hg` module so EdenFS can use it too. Changes:

* Moved `get_opt`, `get_or` and `get_or_default` directly into `ConfigSet`.
* Moved `FromConfigValue` and `ByteCount` into `configparser::convert`.

Reviewed By: quark-zju

Differential Revision: D26355403

fbshipit-source-id: 9096b7b737bc4a0cccee1a3883e89a323f864fac
2021-02-12 12:33:47 -08:00
Arun Kulshreshtha
8412d537f8 pyauth: add bindings to auth crate
Summary: Add Python bindings to the Rust `auth` crate, with the intention of replacing `httpconnection.readauthforuri`.

Reviewed By: quark-zju

Differential Revision: D26419447

fbshipit-source-id: dd13bea74961137790beb8c96120ebef99e3c313
2021-02-12 10:04:27 -08:00
Arun Kulshreshtha
9fc3b326fb bindings/pyerror: add CertificateError exception
Summary:
The auth crate is now able to check the presence and expiration of client certificates (D26009207 (9f7d4447fd)). When a problem is detected, it emits an `X509Error`, which specifies exactly what the problem is. Since this error always indicates a certificate issue, we can print out the message configured in `help.tlsauthhelp` (which is more specific than `help.tlshelp` from the previous diff).

Previously, Mercurial would attempt to use the certificate anyway, resulting in a difficult to understand error message. Although the previous diffs in this stack improved the error messages on any TLS failure, the `X509Error` messages are even more helpful.

Users can opt in to this certificate validation with `edenapi.validate-certs`. The functionality is gated on a config option to prevent Mercurial from crashing if certificates are misconfigured, but EdenAPI isn't being used.

Reviewed By: quark-zju

Differential Revision: D26385843

fbshipit-source-id: 9809f612f8aab3f2dd442d6dd8dc348f1af45296
2021-02-11 19:29:22 -08:00
Arun Kulshreshtha
8b9cf7e7cb bindings/pyerror: add TlsError exception type
Summary: Add a new `TlsError` Python exception type corresponding to `HttpClientError::Tls`.

Reviewed By: quark-zju

Differential Revision: D26385846

fbshipit-source-id: c0df543032461de650a4d24c26c6b8aaab1abbb9
2021-02-11 19:29:21 -08:00
Stefan Filip
93c1231c55 segmented_changelog: update hash_to_location to gracefully handle unknown hashes
Summary:
One of the primary use cases for hash_to_location is translating user provided
hashes. It is then perfectly valid for the hashes that are provided to not
exist.  Where we would previously return an error for the full request if a
hash was invalid, we now omit the hash from the response.

Reviewed By: quark-zju

Differential Revision: D26389472

fbshipit-source-id: c59529d43f44bed7cdb2af0e9babc96160e0c4a7
2021-02-11 12:17:35 -08:00
Stefan Filip
c9f3ae8fa4 edenapi: add commithashtolocation to python client
Summary: Same approach as locationtohash.

Reviewed By: quark-zju

Differential Revision: D26382616

fbshipit-source-id: a06c62c3eebcbe0b07b5c28fce4789a1334d55a4
2021-02-11 12:17:35 -08:00
Stefan Filip
cfed6bb108 edenapi: add commitlocationtohash to python client
Summary:
The approach is very similar to what commitrevlogdata does. You could say
that it's cargo culted.
I am not sure how appropriate it is to return CommitLocationToHashResponse
but I think that it's fine for now.

Reviewed By: quark-zju

Differential Revision: D26374219

fbshipit-source-id: 61d851d5a4fc4223c65078ef434a0c67314a90cd
2021-02-11 12:17:35 -08:00
Durham Goode
5c6edef11c py3: fix simple Windows py3 issues
Summary: These are just a few one-liners to fix Python 3 on Windows.

Reviewed By: sfilipco

Differential Revision: D26381055

fbshipit-source-id: d9257f2cf35c05f931d74b7d26bdc79f5bf34185
2021-02-11 09:50:25 -08:00
Durham Goode
8c08a42d22 dynamicconfig: introduce configs.allowedconfigs
Summary:
In our upcoming migration away from chef/static rc files, we'll be
marking certain files as "allowed". Our hope is that that list only includes
things like .hg/hgrc, ~/.hgrc, etc.

There are cases however where it's convienent to continue to use chef, for
instance when we condition on machine type. To support this, let's add an
allowed_config option, which will allow configs from non-supported locations.

This will also be useful when remediating issues that come up when we start
enforcing allow_location, without rolling back the entire thing.

Reviewed By: quark-zju

Differential Revision: D26233451

fbshipit-source-id: 71789e0361923a6f80de4aef7f012afc0269440d
2021-02-10 19:30:35 -08:00
Andrey Chursin
14064f8582 vfs: move conflict handling from pyworker to vfs write
Summary:
Previously, `write` can fail because the destination file exists as a
directory, or the parent directory is missing. pyworker handles those cases
by calling `clear_conflicts` to remove conflicted directories and create
missing parent directories and retry `write`. Practically, for all `write`
usecases (including checkout) we always want the `clear_conflicts` behavior.
Therefore, move `clear_conflicts` to vfs `write` and make it private.

Reviewed By: quark-zju

Differential Revision: D26257829

fbshipit-source-id: 03d1da0767202edba61c47ae5654847c0ea3b33e
2021-02-09 17:04:30 -08:00
Jun Wu
ae8aa967bb pytracing: set target to module name by default
Summary:
This matches the Rust behavior and is useful for filtering
because the env logger syntax applies to target:

  % RUST_LOG=edenscm.hgext.debugshell=info lhg dbsh
  In [1]: from edenscm import tracing

  In [2]: tracing.info('foo')
  [2021-02-05T19:21:41.082Z INFO  edenscm.hgext.debugshell] message="foo"

Reviewed By: kulshrax

Differential Revision: D26282053

fbshipit-source-id: 8ee9e82b955835b24c49f9bf81c7a3aec7a65a33
2021-02-05 15:19:34 -08:00
Jun Wu
a64040a7e0 revset: optimize nameset._slice
Summary:
This affects `first`, `last`, `limit` revset functions.

Previously, they just iterate through the set naively. Now they have Rust fast
paths. For example:

  In [1]: time repo.revs('last(parents(:1000000),100)')
  CPU times: user 5.08 ms, sys: 1.02 ms, total: 6.1 ms
  Wall time: 4.96 ms
  Out[1]: <nameset+ <spans [0a087e42b29ba5c9ceb3588477d78f7f09ce2663:af5e72c2a0c2e78de462bba8bde63f2499aeb9b5+999900:999999]>>

  In [2]: time repo.revs('first(parents(:1000000),100)')
  CPU times: user 2.21 ms, sys: 40 µs, total: 2.25 ms
  Wall time: 1.83 ms
  Out[2]: <nameset+ <spans [06b96ec2a8b60d984606f36c30d3dbc899d804df:4cab7b68c0bbdc13eb2eded8fc8c4c8d520a7189+0:99]>>

  In [5]: time repo.revs('first(reverse(parents(:1000000)),100)')
  CPU times: user 2.2 ms, sys: 185 µs, total: 2.39 ms
  Wall time: 1.67 ms
  Out[5]: <nameset- <spans [0a087e42b29ba5c9ceb3588477d78f7f09ce2663:af5e72c2a0c2e78de462bba8bde63f2499aeb9b5+999900:999999]>>

  In [6]: time repo.revs('last(reverse(parents(:1000000)),100)')
  CPU times: user 2.01 ms, sys: 12 µs, total: 2.02 ms
  Wall time: 1.68 ms
  Out[6]: <nameset- <spans [06b96ec2a8b60d984606f36c30d3dbc899d804df:4cab7b68c0bbdc13eb2eded8fc8c4c8d520a7189+0:99]>>

  In [7]: time repo.revs('limit(reverse(parents(:1000000)),100,10000)')
  CPU times: user 1.48 ms, sys: 887 µs, total: 2.37 ms
  Wall time: 1.89 ms
  Out[7]: <nameset- <spans [5e23b6f07f1512a8991de5a0e883ff4d598ac1d7:f5f68207be4a458e99d4a9200296977aecf44ea2+989900:989999]>>

This, together with fast `_firstancestors`, could potentially answer
globalrev-like queries on the master branch without recording the globalrev
information server side (which has complexities like locking, etc). For
example, to convert a global rev `g` to commit:

  c = repo.revs('limit(_firstancestors(master), 1, %s)', g).first()

To convert a commit `c` to global rev:

  g = len(repo.revs('_firstancestors(%s)', c)) - 1

Reviewed By: sfilipco

Differential Revision: D26203558

fbshipit-source-id: 14d9247bbb07260f783e05b3fb1034406de48121
2021-02-05 12:00:41 -08:00
Jun Wu
95668fd91d dag: ensure LazySet has Hints set
Summary:
Previously, `LazySet` was constructed with default `Hints`. That disables fast
paths. Revise the API so LazySet requires an explicit `Hints` to address the
issue.

Reviewed By: sfilipco

Differential Revision: D26203561

fbshipit-source-id: c92cd1f7eb7b40ffaaf53abcf05e64f3d41b906d
2021-02-05 11:53:46 -08:00
Jun Wu
9b02ebc711 dag: make spanset module private
Summary:
This just renames types so `IdSet` is the recommended name and `SpanSet`
remains an implementation detail.

Reviewed By: sfilipco

Differential Revision: D26203560

fbshipit-source-id: 7ca0262f3ad6d874363c73445f40f8c5bf3dc40e
2021-02-05 11:53:45 -08:00
Jun Wu
cc123cc1ce revset: optimize _firstancestors using Rust fast path
Summary:
Optimize the `_firstancestors` revset function using Rust.

When calculating `len(_firstancestors(master))`, the new code took 2ms while
the old code needs 76s, a ~40000x improvement.

Reviewed By: sfilipco

Differential Revision: D26182242

fbshipit-source-id: 55f17b014e727d8e8e3099b7c287f8bf8479279b
2021-02-05 11:53:45 -08:00
Jun Wu
01b0122b0b revset: optimize x~n using Rust fast path
Summary:
Optimize the `x~n` revset function using Rust.

Note: This changes the behavior a bit, `x~n` no longer returns `null`.

Reviewed By: sfilipco

Differential Revision: D26142683

fbshipit-source-id: d6a45b7e67352d74986274e52002a769bbae772e
2021-02-05 11:37:51 -08:00
Jun Wu
20603043e2 revset: optimize merge() using Rust fast path
Summary: Optimize the "merge()" revset function using the merges() from Rust.

Reviewed By: sfilipco

Differential Revision: D26142169

fbshipit-source-id: 47f426625869b7889b28bb1a18544d4abae36cae
2021-02-05 11:37:50 -08:00
Jun Wu
6956496602 lib: bytes::Bytes -> minibytes::Bytes
Summary:
The `bytes` crate still does not support zero-copy on mmaped buffer.
Switch to `minibytes::Bytes` so bytes returned from our main storage
backend indexedlog is a zero-copy slice backed by a mmap buffer.

Migrate vfs, revisionstore, pyworker to minibytes so they can preserve
zero-copy mmap buffers from indexedlog.

The edenapi/types is unchanged, since it's also used in Mononoke which uses
`bytes::Bytes` all the places. The conversion to `minibytes::Bytes` is cheap
so it's probably not a performance issue.

Reviewed By: kulshrax

Differential Revision: D26218289

fbshipit-source-id: e4f1c631143b7676c6b48d3b4f97055299bfd334
2021-02-03 20:22:32 -08:00
Durham Goode
88900aaf93 dynamicconfig: remove legacylist and disallowedlist deprecation logic
Summary:
The original migration strategy with dynamicconfig was to fix configs
one by one until the dynamicconfig values matched the chef/static ones, then we
can turn off chef/static configs. This looks to be too much work, so we're going
to try a different strategy of just turning off all chef/static configs on a
small number of hosts and seeing what breaks.

The legacylist and disallowlist configs were part of the old strategy, and they
make it more complicated to fix dynamicconfig mismatches, so let's get rid of
them.

Reviewed By: quark-zju

Differential Revision: D26208548

fbshipit-source-id: 63171f1f16aa0498c0eefa994dffaeb8e0cc0d72
2021-02-03 09:53:00 -08:00
Durham Goode
13de9f801a doctor: repair treemanifest indexedlogs as well
Summary: Previously we only repaired the file indexedlogs.

Reviewed By: xavierd

Differential Revision: D26202423

fbshipit-source-id: b1c673ae69a357d66ab2baf5c36985a3b0597427
2021-02-02 16:21:01 -08:00
Durham Goode
7f555d2d06 http: improve error messages from http failures
Summary:
Currently the data layer eats all errors from remote stores and treats
them as KeyErrors. This hides connection issues from users behind obscure
KeyErrors. Let's make it so that any non-key error reported by the remote store
is propagated up as a legitimate error.

This diff makes Http errors from EdenApi show up with a nicer error message,
suggesting that the user run fixmyserver.

Further fixes will probably be necessary to categorize other errors from the
remote store more nicely.

Reviewed By: quark-zju

Differential Revision: D26117726

fbshipit-source-id: 7d7dee6ec101c6a1d226185bb27423d977096050
2021-01-29 09:40:19 -08:00
Jun Wu
74512659ef pytracing: add instrument
Summary:
Similar to Rust tracing's instrument. It's a decorator for functions that will
generate spans when called.

Reviewed By: sfilipco

Differential Revision: D26021958

fbshipit-source-id: bbb648ab07d6db233cd16f56a5f0441df07e1c2e
2021-01-28 13:17:59 -08:00
Jun Wu
9b4732a145 pytracing: expose APIs about runtime span callsites and spans
Summary:
This allows Python to create span callsites understood by Rust tracing,
and operate on the spans (ex. enter, exit, record, etc.).

Reviewed By: sfilipco

Differential Revision: D26013747

fbshipit-source-id: 2783b29750e3279c5481422bddc83366ff7a3548
2021-01-28 13:17:59 -08:00
Jun Wu
1d314204e3 pytracing: expose APIs about runtime event callsites
Summary:
This allows Python to create callsites for one-off events understood by Rust.
This diff adds the "EventCallsite" for logging one-off events.

Reviewed By: sfilipco

Differential Revision: D26013749

fbshipit-source-id: 2520928dc360852afe2780267036e9d22c212191
2021-01-28 13:17:59 -08:00
Thomas Orozco
9d7b0267dd revisionstore: pass client correlator
Summary:
We used to get those in the old (Python) LFS extension, but didn't have them in
the new one. However, this is helpful to correlate requests to LFS with data in
hg logs. It's also convenient to be able to identify whether a set of requests
are part of the same session or not.

This diffs threads the client correlator through to the LFS store from the
Python, similarly to how it's done in EdenAPI.

Reviewed By: DurhamG

Differential Revision: D25804930

fbshipit-source-id: a5d5508617fa4184344834bbd8e3423816aa7668
2021-01-11 10:46:20 -08:00
Stefan Filip
02606da6c5 pathmatcher: allow errors in match function definition
Summary:
The most common scenario where we see matcher errors is when we iterate through
a manifest and the user sends SIGTERM to the process. The matcher may be both
Rust and Python code. The Python code handles the interrupt and prevents future
function calls. The iterating Rust code will continue to call matcher functions
through this time so we get matcher errors from the terminated Python stack.

As long as we have Python matcher code, errors are valid.
It is unclear to me whether the matcher trait should have `Result` return
values when all implementations are Rust. It is easy to imagine implementations
that can fail in different circumstances but the ones that we use if we just
port the Python code wouldn't fail.
All in all, I think that this is a reasonable step forward.

Reviewed By: quark-zju

Differential Revision: D25697099

fbshipit-source-id: f61c80bd0a8caa58040a447ed02d48a1ae84ad60
2021-01-07 16:22:17 -08:00
Jun Wu
fab3b21289 pydag: expose Dag and IdMap ID via hints
Summary: This makes it easier to investigate fast path issues.

Reviewed By: sfilipco

Differential Revision: D25598077

fbshipit-source-id: 27b7042fb9510321c25371f8c5d134e248b3d5d5
2020-12-18 16:56:43 -08:00
Jun Wu
207f755dc0 hgcommits: make revlog optional for the hybrid backend
Summary: This makes it possible to add new commits in a repo without revlog.

Reviewed By: sfilipco

Differential Revision: D25602527

fbshipit-source-id: 56c27a5f00307bcf35efa4517c7664a865c47a43
2020-12-18 16:47:11 -08:00
Durham Goode
dce0faa9b1 configparser: add allowed_location criteria to config verifier
Summary:
We want to start disallowing non-approved config files from being
loaded. To do that, let's update the config verifier to accept an optional list
of allowed locations. If it's provided, we delete any values that came from a
disallowed location.

This will enable us to prune our config sources down to rust configs,
configerator configs, .hg/hgrc, and ~/.hgrc.

Reviewed By: quark-zju

Differential Revision: D25539738

fbshipit-source-id: 0ece1c7038e4a563c92140832edfa726e879e498
2020-12-17 06:37:54 -08:00
Jun Wu
843ef6aab2 pydag: add API to get commit text in batch
Summary:
Unlike streamcommitrawtext, the new API does not put Python logic to a
background thread. This will make it easier to reason about Python logic as
they do not need to be thread-safe, and we don't need to think about Python GIL
deadlocks in the Rust async world.

Reviewed By: sfilipco

Differential Revision: D25513057

fbshipit-source-id: 4b30d7bab27070badd205ac1a9d54bae7f1f8cec
2020-12-14 19:10:44 -08:00
Xavier Deguillard
49de273c77 revisionstore: handle redacted blob in LFS
Summary:
When a blob is redacted server side, the http code 410 is returned.
Unfortunately, that HTTP code wasn't supported and caused Mercurial to crash.
To fix this, we just need to store a placeholder when receiving this HTTP code
and simply return it up the stack when reading it.

Reviewed By: DurhamG

Differential Revision: D25433001

fbshipit-source-id: 66ec365fa2643bcf29e38b114ae1fc92aeaf1a7b
2020-12-11 12:28:38 -08:00
Jun Wu
ad6f25addc dag: make IdConvert async
Summary: Make IdConvert async and migrate all its users.

Reviewed By: sfilipco

Differential Revision: D25350915

fbshipit-source-id: f05c89a43418f1180bf0ffa573ae2cdb87162c76
2020-12-10 12:37:35 -08:00
Jun Wu
461fa77fd7 dag: make Set::flatten async
Summary: This will make it easier to make IdConvert async.

Reviewed By: sfilipco

Differential Revision: D25345239

fbshipit-source-id: 684a0843ae32270aa9b537ef9a2b17a28c027e51
2020-12-10 12:37:34 -08:00
Jun Wu
53bdae78d9 dag: make ToIdSet async
Summary: This will make it easier to make IdConvert async.

Reviewed By: sfilipco

Differential Revision: D25345232

fbshipit-source-id: b8967ea51a6141a95070006a289dd724522f8e18
2020-12-10 12:37:34 -08:00
Jun Wu
f854d2e03e dag: make DagAlgorithm async
Summary:
Update DagAlgorithm and all its users to async. This makes it easier to make
IdConvert async.

Reviewed By: sfilipco

Differential Revision: D25345236

fbshipit-source-id: d6cf76723356bd0eb81822843b2e581de1e3290a
2020-12-10 12:37:34 -08:00