Commit Graph

1251 Commits

Author SHA1 Message Date
Michael Bolin
b83240eb52 Put together a doc to explain the contracts of hooks in Mononoke.
Summary:
This is based on my reading of the source code, so I may have gotten things
wrong. Feel free to editorialize!

By putting this in a doc, hopefully it makes it easier for us to reason about
the API at a high level. Obviously it would be great if we could keep this up
to date going forward.

Reviewed By: StanislavGlebik

Differential Revision: D10340604

fbshipit-source-id: 9f3e82d234842e06c52f8a8b4440f8e06c487c0b
2018-10-19 11:49:56 -07:00
Pavel Aslanov
653a77a998 correctly handle case conflicting reenames
Summary:
Correctly handles case conflicting renames (only change in casing).
-  path can now be removed from `CaseConlictingTrie`
- `check_case_conflicts` operates on `BonsaiChangeset` in pushrebase logic

Reviewed By: StanislavGlebik

Differential Revision: D10447522

fbshipit-source-id: d5342e7aa48154debee123b38bf3168e3371baa6
2018-10-19 04:19:56 -07:00
Stanislau Hlebik
bba987bf19 mononoke: fix conflict_markers hook
Summary:
It was broken because it only matched conflict markers that were on the first
line. This diff fixes it by splitting the file content by \n first

Reviewed By: farnz

Differential Revision: D10447393

fbshipit-source-id: a2091f6bc43e8bb9a77c63536e749432d524bbff
2018-10-19 02:41:22 -07:00
Michael Bolin
82e2435704 Port gitattributes-textdirectives hook to Mononoke
Summary:
This hook is designed to prevent text directives in .gitattributes
from making it into the repo.

As noted in the integration test, our regex may be too loose,
but it's probably OK, in practice.

For better or worse, for now, we're just trying to maintain the
behavior of the existing hook (though perhaps the existing hook
would have been a bit stricter if it wren't written in Bash).

For easy reference, here are the Git docs on gitattributes:
https://git-scm.com/docs/gitattributes/

Reviewed By: StanislavGlebik

Differential Revision: D10387336

fbshipit-source-id: c58f689ecc0648c2cc359a818c92d701258e8f46
2018-10-19 00:50:00 -07:00
Simon Farnsworth
2d3bc65e05 Add a deny_files hook, like the one in Mercurial
Summary:
We want to deny landing files whose path contains magic strings. Add a
hook to do this, with some predefined examples of how to write patterns

Reviewed By: StanislavGlebik

Differential Revision: D10446531

fbshipit-source-id: 67f1a712d923345288c8d0a4f3e5da1e8f4e29f8
2018-10-18 13:32:00 -07:00
Lukas Piatkowski
9a23a6ceaf server: use the same tokio Runtime to load configs and to serve incoming requests
Summary:
Using multiple Runtimes might be a cause of problems in future and even if it isn't it will be a cause for investigating whether it is a problem or not.
The issue I have in mind is that if someone runs a future on one runtime that calls `tokio::spawn` on it (f.e. schedule a job that works for ever) but then uses a different runtime to drive another future to complection one might not suspect that the previous spawn is already lost with the previous Runtime.

Reviewed By: farnz

Differential Revision: D10446122

fbshipit-source-id: 4bfd2a04487a70355a26f821e6348f5223901c0d
2018-10-18 08:17:19 -07:00
Lukas Piatkowski
28bb85795e Back out "filenodes: revert using myrouter, use dieselfilenodes again"
Summary: Original commit changeset: 07da917455ae

Reviewed By: farnz

Differential Revision: D10446126

fbshipit-source-id: 918f77873cfb35744e489d9afb8b630764cbb199
2018-10-18 08:17:19 -07:00
Stanislau Hlebik
fb425906e0 mononoke: run hooks in parallel
Summary:
Previously buffered() wasn't particularly useful because it buffered only
mapping from ChangesetId to HgChangesetId. The actual running of hooks was done in
`.and_then()` and that means that each future in the stream should finish
before the next one starts.

Let's put running of hooks inside a buffer, that helps with perf a lot.

Reviewed By: jsgf

Differential Revision: D10359546

fbshipit-source-id: 48b8b200d7397eef8622c32cad9cec889b96f9d0
2018-10-17 08:06:06 -07:00
Stanislau Hlebik
bfe352bae4 mononoke: change logging in hook_tailer
Summary:
Let's change level of logging for each hook/commit to debug, since it was
spammy. Instead let's print a total statistics about how many hooks were
accepted/rejected

Reviewed By: jsgf

Differential Revision: D10358786

fbshipit-source-id: 2e451d482ed5549e41975f9e3b57b05d90069788
2018-10-17 08:06:06 -07:00
Stanislau Hlebik
eaf156206c mononoke: make continuous running optional
Summary:
Let's add an option `--continuous`. If it's not specified then hook tailer runs
only once. That's useful for testing the new hooks.

Reviewed By: jsgf

Differential Revision: D10358785

fbshipit-source-id: b62d01b4bf3233c3f411fc298fefb79da473d7f1
2018-10-17 08:06:05 -07:00
Stanislau Hlebik
bf740d2006 mononoke: fix retry_num counting in pushrebase
Summary: Previously it always returned 1. This diff fixes it

Reviewed By: farnz

Differential Revision: D10401362

fbshipit-source-id: feceba24ca8931555b11ad29164127c4015ec751
2018-10-17 08:03:04 -07:00
Tim Fox
880dd6b125 Add tp2_symlinks_only hook
Summary: ${title}

Reviewed By: StanislavGlebik

Differential Revision: D10423277

fbshipit-source-id: 3246530b6e1011e4d21fa2c088b51b73174368fb
2018-10-17 07:23:19 -07:00
Tim Fox
35cbe06f80 Expose is_sym_link function in hooks
Summary: ${title}

Reviewed By: StanislavGlebik

Differential Revision: D10423278

fbshipit-source-id: b14cee2f5640cc7152d54506371ce452776749e4
2018-10-17 07:23:19 -07:00
Anastasiya Zhyrkevich
d2a4f4e042 getfiles, config lfs threshold
Summary:
getfiles implementation for lfs

The implementation is the following:
- get file size from file envelope  (retrieve from manifold by HgNodeId)
- if file size > threshold from lfs config
   - fetch file to memory, get sha256 of the file, will be fixed later, as this approach consumes a lot of memory, but we don't have any mapping from sha256 - blake2 [T35239107](https://our.intern.facebook.com/intern/tasks/?t=35239107)
   - generate lfs metadata file according to [LfsPlan](https://www.mercurial-scm.org/wiki/LfsPlan)
   - set metakeyflag (REVID_STORED_EXT) in the file header
- if file size < threshold, process usual way

Reviewed By: StanislavGlebik

Differential Revision: D10335988

fbshipit-source-id: 6a1ba671bae46159bcc16613f99a0e21cf3b5e3a
2018-10-17 02:20:06 -07:00
Lukas Piatkowski
f377fb4ed7 filenodes: revert using myrouter, use dieselfilenodes again
Summary: Reverting the myrouter based filenodes for now as they cause some problems

Reviewed By: jsgf

Differential Revision: D10405364

fbshipit-source-id: 07da917455ae5af9ef81a24d99f516171101c8a7
2018-10-16 09:53:21 -07:00
Anastasiya Zhyrkevich
885960087e LFS push: processing files in metadata format from hg client
Summary:
According to [Mercurial Lfs Plan](https://www.mercurial-scm.org/wiki/LfsPlan), on push, for files which size is above the threshold (lfs.threshold config) hg client is sending LFS metadata instead of actual files contents. The main part of LFS metadata is SHA-256 of the file content (oid).

The format requires the following mandatory fields: version, oid, size.

When lfs metadata is sent instead of a real file content then lfs_ext_stored flag is in the request's revflags.
If this flag is set, We are ignoring sha-1 hash verification inconsistency.
Later check that the content is actually loaded to the blobstore and create filenode envelope from it, load the envelope to the blobstore.

Filenode envelope requires the following info:
- size - retrieved on fetching the actual data from blobstore.
- copy_from - retrieved from the file, sent by hg client.

Mononoke still does the same checks for LFS push as for non-lfs push (i.e. checks that all the necessary manifests/filelogs were uploaded by a client)

Reviewed By: StanislavGlebik

Differential Revision: D10255314

fbshipit-source-id: efc8dac4c9f6d6f9eb3275d21b7b0cbfd354a736
2018-10-16 04:24:20 -07:00
Jeremy Fitzhardinge
b4f0a1e7ce tp2: update rust-crates-io
Summary:
Add libnfs, libffi and starlark.

Also nom now has "verbose-errors" feature (via bindgen -> cexpr -> nom), so make some tweaks to cope.

Reviewed By: farnz

Differential Revision: D10371391

fbshipit-source-id: ba889ad16a7b662c5eddafcb1e705b068ccc9af7
2018-10-15 23:08:01 -07:00
Michael Bolin
53939d9b72 Simplify __set_common_file_functions.
Summary:
This is my first Lua code, but I think this should work?
I'm assuming there's no overhead of accessing `type` from a closure that we
need to worry about?

Reviewed By: StanislavGlebik

Differential Revision: D10338505

fbshipit-source-id: 497da36c9ce56da50fb5f5cf26fcd8d8340b18c2
2018-10-15 13:52:37 -07:00
Anastasiya Zhyrkevich
629a272a00 apiserver test fix for different http versions
Summary:
Fix apiserver output for different HTTP/*
Caused by adding new rust crates for http2

Connected to constant test failing
https://our.intern.facebook.com/intern/testinfra/testconsole/testrun/1125899967685079/

Reviewed By: kulshrax

Differential Revision: D10379812

fbshipit-source-id: 7200242bb0f1a96e4f6bec0b3c714005a28a77ec
2018-10-15 09:37:36 -07:00
Simon Farnsworth
2c42af7925 mononoke: fix simultaneous pushrebase
Summary:
Previously `create_rebased_changesets` was passed incorrect `root` value in case
it was a second or later retry attempt. It was passed the new value of `onto`
bookmark. And then in `create_rebased_changesets` it was used to rebase from
an ancestor of a root changeset to a new bookmark value. But since it wasn't
actually a root value but a new bookmark value, then the commit wasn't rebased
at all.

This diff fixes it

Reviewed By: aslpavel

Differential Revision: D10373516

fbshipit-source-id: dd5ce696d0dd2efaab3886e5ec17d51da4c82078
2018-10-15 04:47:42 -07:00
Tim Fox
11d3616891 block_cross_repo_commits hook
Summary: Implement $title

Reviewed By: farnz

Differential Revision: D10378242

fbshipit-source-id: b6ca253b32a77e0b63c84dc5c16698cc69cd4713
2018-10-15 04:25:51 -07:00
Pavel Aslanov
38c5145e9b hadle change only in executable bit same way as Hg
Summary:
Mercurial stores executable bit as part of the manifest, so if changeset only changes that attribute of a file Hg reuses file hash. But mononoke has been creating additional file node. So this change tries to handle this special case. Note this kind of reuse only happens if file has only one parent [P60183653](P60183653)

Some of our fixtures repo were effected, hence this hashes were replaced with updated ones
```
396c60c14337b31ffd0b6aa58a026224713dc07d => a5ab070634ab9cbdfc92404b3ec648f7e29547bc
339ec3d2a986d55c5ac4670cca68cf36b8dc0b82 => c10443fa4198c6abad76dc6c69c1417b2e821508
b47ca72355a0af2c749d45a5689fd5bcce9898c7 => 6d0c1c30df4acb4e64cb4c4868d4c974097da055
```

Reviewed By: farnz

Differential Revision: D10357440

fbshipit-source-id: cdd56130925635577345b08d8ed0ae6e229a82a7
2018-10-15 02:16:50 -07:00
Arun Kulshreshtha
c2de99cd02 Update rust-crates-io to add HTTP/2 crates
Summary:
Updates rust-crates-io to add several HTTP/2 related crates, namely, `h2`, `httpbis`, and `curl`.

Additionally, fix any breakages introduced by new crate versions. (In particular, the `blake2` crate had breaking changes from 0.7.1 -> 0.8.)

Reviewed By: DurhamG

Differential Revision: D10346164

fbshipit-source-id: 6805261542b5b9c46a34cad6cf6e9fe38f074e87
2018-10-11 15:10:23 -07:00
Lowik Chanussot
33c25691f3 Make ManifestMissing error accept HgManifestId
Summary:
- ManifestMissing accepts HgManifestId instead of HgNodeHash
- Update calls to ManifestMissing
- Update unit test accordingly

Reviewed By: StanislavGlebik

Differential Revision: D10337392

fbshipit-source-id: b70ac6381043cbf64ec7cdafbf338c2af1e00076
2018-10-11 14:20:42 -07:00
Lukas Piatkowski
44ad7ef408 sql: add wait_for_myrouter async function that the client can chain on to wait for myrouter to startup
Summary:
As per the comments added - MyRouter setup is such that it starts inside a tupperware container together with the binary that will be using it. This means that by the time the binary wants to use the MyRouter connection the MyRouter instance might not be ready yet. In order to mitigate this effect the myrouter::Builder will attempt to make a "Select 1" query and retry it with a backoff for a max of 2 min or until the connection is actually established.

Unfortunately the `queries!` macro had to be moved inside the `macro` module in order to make it usable from inside `myrouter` module, see this: https://stackoverflow.com/questions/31103213/import-macro-from-parent-module

Reviewed By: farnz

Differential Revision: D10270464

fbshipit-source-id: 9cf6ad936a0cabd72967fb96796d4af3bab25822
2018-10-11 10:52:05 -07:00
Lukas Piatkowski
cad69fedd0 filenodes: use sqlfilenodes instead of dieselfilenodes; pass myrouter_port around
Reviewed By: farnz

Differential Revision: D10338868

fbshipit-source-id: 60734d9635df442691cad3637aebd5bc838e03ad
2018-10-11 10:52:05 -07:00
Lukas Piatkowski
6de0ccc24e filenodes: add sqlfilenodes implementation using common/rust/sql instead of diesel
Reviewed By: farnz

Differential Revision: D10261151

fbshipit-source-id: fdae33f370123cd968e9ee6ef0e20c55d9f6e88b
2018-10-11 10:52:05 -07:00
Lukas Piatkowski
dd79d028dd filenodes: fix dieselfilenodes-cmd by calling it's code inside tokio
Reviewed By: farnz

Differential Revision: D10251659

fbshipit-source-id: 6f288a41d63d25521efd41349c4bd34b77a2a40b
2018-10-11 10:52:05 -07:00
Lowik Chanussot
395b124f5d Make get_manifest_by_nodeid accept HgManifestId
Summary: Make get_manifest_by_nodeid accept HgManifestId and correct all calls to get_manifest_by_nodeid.

Reviewed By: StanislavGlebik

Differential Revision: D10298425

fbshipit-source-id: 932e2a896657575c8998e5151ae34a96c164e2b2
2018-10-11 06:50:16 -07:00
Stanislau Hlebik
3c077a3875 mononoke: fix hook tailer
Summary: It got a bit rusty, this diff adds missing stuff

Reviewed By: farnz

Differential Revision: D10302729

fbshipit-source-id: af598f8c8fdd5c938c07052c03ab0f84fc6d3c20
2018-10-11 05:50:48 -07:00
Stanislau Hlebik
b298a691ca mononoke: fix changed file calculation
Summary:
One brainless idiot decided to prune all trees from changed files calcualation.
Since it also prunes subtrees, that leaves with just files in the root
directory.

Reviewed By: lukaspiatkowski

Differential Revision: D10302299

fbshipit-source-id: 8fe2c4ad8de998dfd4083d97cd816d85b5fec604
2018-10-11 05:50:48 -07:00
Stanislau Hlebik
382c1e8d31 mononoke: conflict_markers hook
Summary:
Hooks that makes sure that there are no conflict markers in file contents.
This hook is bypassable.

Reviewed By: purplefox

Differential Revision: D10260230

fbshipit-source-id: b9d69e757f18ed3f4f889a01032ef7360cba6867
2018-10-11 05:50:48 -07:00
Stanislau Hlebik
d5a8790a56 mononoke: slightly better printing of hook failures
Summary: Not a final version for sure, just a small improvement

Reviewed By: lukaspiatkowski

Differential Revision: D10260231

fbshipit-source-id: 9f9f61f23da5ac9a5d1abc9ad2f50900ca434326
2018-10-11 05:50:48 -07:00
Stanislau Hlebik
a79d9d4a25 mononoke: add pushvars bypasses
Summary: Pushvars is a one more way to bypass hooks. This diff implements it

Reviewed By: purplefox

Differential Revision: D10257602

fbshipit-source-id: 1bd188239878ff917ded7db995ea2453da9f64c4
2018-10-11 05:50:48 -07:00
Stanislau Hlebik
ceba87afec mononoke: add commit message bypasses
Summary:
Let's add a logic to allow users to bypass hooks.

We'll have two ways to bypass hooks. One is via a string in commit message,
another is via pushvars.
This diff implements the first one.

Reviewed By: purplefox

Differential Revision: D10255378

fbshipit-source-id: 31e803a58e2f4798294f7c807933c8e26de3cfaf
2018-10-11 05:50:47 -07:00
Lukas Piatkowski
177bbc024e cmdlib: be able to parse --myrouter-port argument and ignore it
Summary: also includes a fix for blobimportjob passing an empty argument to blobimport when was not used

Reviewed By: farnz

Differential Revision: D10298965

fbshipit-source-id: 1e9475c53a7e0b7ea211cc8fbd2c327327b66c65
2018-10-10 08:21:18 -07:00
Tim Fox
370c9f9bd9 Add hook method to get any file content
Summary: Add a new method for changeset hooks to allow the content of any file to be retrieved.

Reviewed By: StanislavGlebik

Differential Revision: D10255914

fbshipit-source-id: 4ec89369835a2807675b1eda41b4399cf0c66b32
2018-10-10 03:35:24 -07:00
Lukas Piatkowski
1d69b1f884 mononoke: add a flag for --myrouter-port, ignore it for now
Summary:
The idea for rollout is to:
- first make sure that Mononoke doesn't crash when a --myrouter-port is provided
- then tupperware configs will be modified to include myrouter as a collocated proces on every host and the port of that myrouter instance will be provided via command line
- lastly land the change that actually talks to myrouter

Reviewed By: StanislavGlebik

Differential Revision: D10258251

fbshipit-source-id: ea9d461b401d41ef624304084014c2227968d33f
2018-10-09 10:21:04 -07:00
Stanislau Hlebik
f9cdae09c6 mononoke: replace PerFile hook with PerAddedOrModified
Summary:
That's a bit controversial, however I think it's worth it. Many file hooks
should be run only on files that exist in the repo (for example,
https://fburl.com/1cj8wm3p, https://fburl.com/t06fjwak). If you want to do
anything on deleted files then just write a changeset hook.

Reviewed By: jsgf

Differential Revision: D10239186

fbshipit-source-id: 3cb563b81ec51298623cecaf976b5a8fe50dc71c
2018-10-09 02:05:55 -07:00
Lukas Piatkowski
0f24377899 rust-crates-io: add crossbeam to tp2
Reviewed By: ikostia

Differential Revision: D10244968

fbshipit-source-id: 8d06bb64b6a1227ae589caf0588a1f3657603ce9
2018-10-08 21:36:00 -07:00
Anastasiya Zhyrkevich
c4f7fae0bb LFS test from hg client to Mononoke server, with large files through
Summary:
Test is failing, as Mononoke server lfs support is not implemented yet.
Integration test for commands from hg client to Mononoke server.

\s(re) lines are added as after auto-save, the test script is formatted, and delete spaces at the empty lines.
In order to keep such lines, \s(re) could be added
In comparison of such line, pattern \s(re) is deleted and not compared.
See to mononoke/tests/integration/third_party/hg_run_tests.py for more information about comparison of the output lines.

Reviewed By: StanislavGlebik

Differential Revision: D10089289

fbshipit-source-id: 2962e80d919c21801d08990be190f2574c48646d
2018-10-08 16:05:59 -07:00
Stanislau Hlebik
9685ca7c45 mononoke: actually pass comments
Summary: Fix copy-paste error

Reviewed By: farnz

Differential Revision: D10237928

fbshipit-source-id: 18ecfeefe5506ac51d621d8be0796565d11d3794
2018-10-08 09:06:26 -07:00
Stanislau Hlebik
c3b5ee6854 mononoke: add (re) for lines ending with whitespace in integration tests
Summary:
Many editors remove trailing whitespaces on save. That makes modifying these
files annoying. Adding ` (re)` mitigates the issue

Reviewed By: farnz

Differential Revision: D10237590

fbshipit-source-id: 1473f35023b878f21ff22bd5a5ccb5f11884cef3
2018-10-08 09:06:26 -07:00
Stanislau Hlebik
d95fa203ce mononoke: pass change file type information to hooks
Summary:
Hooks need to know whether file was added, modified or removed. For example, we
can't fetch content of a removed file. Also hook authors may want to allow
modifying existing files of a particular type, but they may want to disallow
addition of new files of this type.

`cs.files()` doesn't give information about whether a file was
added/deleted/modified, so we have to use `get_changed_stream` function from
manifest_utils.

Note - currently it still returns incorrect list of changed files for merges.
It will be fixed in the next diffs.

Reviewed By: farnz

Differential Revision: D10237587

fbshipit-source-id: cd7f76334070cde451b4690071d03275e40c95f3
2018-10-08 09:06:26 -07:00
Stanislau Hlebik
a1b12f6beb mononoke: remove broken assert
Summary:
This assert was just broken - it's fine to call `get_full_path()` on file
entry. What's disturbing is that there were no tests that cover this behaviour
i.e. no tests returned modified file!

This diff fixes both problems

Reviewed By: farnz

Differential Revision: D10237589

fbshipit-source-id: dcb7f1977768262491b4624a30a5e861c3c1eadf
2018-10-08 09:06:25 -07:00
Anastasiya Zhyrkevich
c9e390669d tests for changegroup pack/unpack for version 3
Summary:
Tests are for changegroup version3
Unitests in part_inner.rs
QuickCheck for Ch3Packer and Unpacker.

Packer has the same implementation, as Packer packs everything operating with chunks, and does not care about sections.

In quickcheck tests, my proposal is to have similar CgSeqGenerator for both of Cg versions.
But as sections are different, to introduce optional sections.

Reviewed By: StanislavGlebik

Differential Revision: D10192025

fbshipit-source-id: bef0d654724f18177461c6326324d6972943eb23
2018-10-05 02:51:24 -07:00
Anastasiya Zhyrkevich
8dd9b3f937 parser for change group version 3
Summary:
Support ChangeGroup version 3 from Mercurial

on Push, Hg client sends bundle, which is encoded in type 2 or type 3.
The type depends on which extentions are turned on in client.

The type is written in PartHeader, part_type: B2xRebase, aparams: {"cgversion": b"03"}

The differences between type 2 and type 3:
- could be found in fbcode/scm/hg/mercurial/help/internals/changegroups.txt
- The flow of parts is different. There is a new part in type 3:
-- type 2: Changeset, Manifest,                        Filename, Filelog
-- type 3: Changeset, Manifest, Treemanifest, Filename, Filelog
- According to hg implementation, Treemanifest part should always be empty (consists of Section End chunk (4 bytes of zeores) )
- Different chunk header size, as flags (unsigned int) is added. (type 2 - 100 bytes, type3 - 102 bytes in total)

Mononoke should support both versions, as if  LFS is turned on, hg client is sending bundles in version 3 type.

Reviewed By: StanislavGlebik, farnz

Differential Revision: D10162321

fbshipit-source-id: f2ae664206b147db5f24312942d7fcf89ccd69b3
2018-10-05 02:51:23 -07:00
Simon Farnsworth
6e2455a12e Hook up (untested) support for streaming clones
Summary:
We now have a way for a MySQL database to tell us how to send
streaming clones to the client. Hook it all up, so that (with any luck), once
we have data in MySQL and the blobstore, we'll see working streaming clones.

Reviewed By: StanislavGlebik

Differential Revision: D10130774

fbshipit-source-id: b22ffb642d0a54b09545889779f79e7a0f81acd7
2018-10-04 11:37:46 -07:00
Pavel Aslanov
c920dcc0a6 fix copy metadata generation
Summary:
We used generate copy metadata which  mercurial understand but works a bit differently
Relevant mercurial code:
```
def packmeta(meta, text):
    keys = sorted(meta)
    metatext = "".join("%s: %s\n" % (k, meta[k]) for k in keys)
    return "\1\n%s\1\n%s" % (metatext, text)
```

Reviewed By: StanislavGlebik

Differential Revision: D10200805

fbshipit-source-id: f17a51a6aac2e1d3671fbbf3e969ed747e2fce18
2018-10-04 10:08:40 -07:00
Tim Fox
2b90a41b8b Add hook method to get file content
Summary: Add a new hook method to return file contents

Reviewed By: lukaspiatkowski

Differential Revision: D10029268

fbshipit-source-id: c7ba41f6273e920ee398e15b6461a4b422f0420b
2018-10-02 07:07:16 -07:00