├── .editorconfig
├── .github
├── CODEOWNERS
├── dependabot.yml
├── settings.yml
└── workflows
│ ├── build.yaml
│ └── upgrade-flakes.yaml
├── .gitignore
├── CONTRIBUTING.md
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── flake.lock
├── flake.nix
├── scripts
├── dura.fish
└── pre-commit.sh
├── src
├── config.rs
├── database.rs
├── git_repo_iter.rs
├── lib.rs
├── log.rs
├── logger.rs
├── main.rs
├── metrics.rs
├── poll_guard.rs
├── poller.rs
└── snapshots.rs
└── tests
├── poll_guard_test.rs
├── snapshots_test.rs
├── startup_test.rs
├── util
├── daemon.rs
├── dura.rs
├── git_repo.rs
├── macros.rs
└── mod.rs
└── watch_test.rs
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 | indent_style = space
13 | indent_size = 4
14 |
15 | [*.md]
16 | # double whitespace at end of line
17 | # denotes a line break in Markdown
18 | trim_trailing_whitespace = false
19 |
20 | [{*.yml, *.yaml, *.nix}]
21 | indent_size = 2
22 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @tkellogg
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
--------------------------------------------------------------------------------
/.github/settings.yml:
--------------------------------------------------------------------------------
1 | # https://github.com/probot/settings
2 |
3 | branches:
4 | - name: master
5 | protection:
6 | enforce_admins: false
7 | required_pull_request_reviews:
8 | dismiss_stale_reviews: true
9 | require_code_owner_reviews: true
10 | required_approving_review_count: 1
11 | required_status_checks:
12 | strict: true
13 | restrictions: null
14 | required_linear_history: true
15 |
16 | labels:
17 | - name: backward breaking change
18 | color: ff0000
19 |
20 | - name: bug
21 | color: ee0701
22 |
23 | - name: dependencies
24 | color: 0366d6
25 |
26 | - name: enhancement
27 | color: 0e8a16
28 |
29 | - name: experimentation
30 | color: eeeeee
31 |
32 | - name: question
33 | color: cc317c
34 |
35 | - name: new feature
36 | color: 0e8a16
37 |
38 | - name: security
39 | color: ee0701
40 |
41 | - name: stale
42 | color: eeeeee
43 |
44 | repository:
45 | allow_merge_commit: true
46 | allow_rebase_merge: true
47 | allow_squash_merge: true
48 | default_branch: main
49 | description: "You shouldn't ever lose your work if you're using Git"
50 | topics: git
51 | has_downloads: true
52 | has_issues: true
53 | has_pages: false
54 | has_projects: false
55 | has_wiki: false
56 | name: dura
57 | private: false
58 |
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on: [ workflow_call, workflow_dispatch, push, pull_request ]
4 |
5 | jobs:
6 | build:
7 | name: build
8 | runs-on: ${{ matrix.os }}
9 | strategy:
10 | matrix:
11 | include:
12 | - os: ubuntu-latest
13 | binary-suffix: linux-x86_64
14 | - os: macos-latest
15 | binary-suffix: macos-x86_64
16 | - os: windows-latest
17 | binary-suffix: windows-x86_64
18 | os: [ ubuntu-latest, macos-latest, windows-latest ]
19 |
20 | steps:
21 | - name: Check out source files
22 | uses: actions/checkout@v3
23 | with:
24 | fetch-depth: 1
25 |
26 | - name: Update Toolchain
27 | uses: actions-rs/toolchain@v1
28 | with:
29 | toolchain: stable
30 | components: clippy
31 |
32 | - name: Build
33 | uses: actions-rs/cargo@v1
34 | with:
35 | command: build
36 | args: --release --all-features
37 |
38 | - name: Rustfmt
39 | uses: actions-rs/cargo@v1
40 | with:
41 | command: fmt
42 | args: --check
43 |
44 | - name: Cargo Clippy
45 | uses: actions-rs/cargo@v1
46 | with:
47 | command: clippy
48 | args: --all-targets --all-features -- -D warnings
49 |
50 | - name: Cargo Test
51 | uses: actions-rs/cargo@v1
52 | with:
53 | command: test
54 | args: --profile release
55 |
56 | - name: Basic Testing
57 | continue-on-error: true
58 | if: ${{ matrix.os != 'windows-latest' }}
59 | run: |
60 | ${{ github.workspace }}/target/release/dura serve &
61 | sleep 15s
62 | ${{ github.workspace }}/target/release/dura kill
63 |
64 | - name: Basic Testing (Windows)
65 | continue-on-error: true
66 | if: ${{ matrix.os == 'windows-latest' }}
67 | run: |
68 | Start-Process -NoNewWindow ${{ github.workspace }}\target\release\dura.exe serve
69 | Start-Sleep -s 15
70 | ${{ github.workspace }}\target\release\dura.exe kill
71 |
72 | - name: Upload Binary
73 | uses: actions/upload-artifact@v3
74 | with:
75 | name: dura-${{ matrix.binary-suffix }}_${{ github.sha }}
76 | path: ${{ github.workspace }}/target/release/dura
77 |
78 |
--------------------------------------------------------------------------------
/.github/workflows/upgrade-flakes.yaml:
--------------------------------------------------------------------------------
1 | name: 'Update flake lock file'
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * 1'
6 |
7 | jobs:
8 | createPullRequest:
9 | uses: loophp/flake-lock-update-workflow/.github/workflows/upgrade-flakes.yaml@main
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled files and executables
2 | /target/
3 | debug/
4 |
5 | # backup files from rustfmt
6 | **/*.rs.bk
7 |
8 | # debugging information from rustc on MSVC Windows
9 | *.pdb
10 | /.idea/
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to `dura`
2 |
3 | # Pull request process
4 | 1. Discuss changes before starting. This helps avoid awkward situations, like where something has already been tried or isn't feasible for a non-obvious reason.
5 | 2. Add tests, if possible
6 | * [`startup_test.rs`](https://github.com/tkellogg/dura/blob/master/tests/startup_test.rs) is a good place to test out new functionality, and the test code reads fairly well.
7 | * Unit tests are preferred, when feasible. They go inside source files.
8 | 3. Run `$ ./scripts/pre-commit.sh` before pushing. This does almost everything that happens in CI, just faster.
9 | 4. Explain the behavior as best as possible. Things like screenshots and GIFs can be helpful when it's visual.
10 | 5. Breathe deep. Smell the fresh clean air.
11 |
12 | We try to get to PRs within a day. We're usually quicker than that, but sometimes things slide through the cracks.
13 |
14 | Oh! And please be kind. We're all here because we want to help other people. Please remember that.
15 |
16 |
17 | # Coding guidelines
18 |
19 | ## Printing output
20 | * All `stdout` is routed through the logger and is JSON.
21 | * Messages to the user should be on `stderr` and are plain text (e.g. can't take a lock)
22 | * Use serialized structs to write JSON logs, so that the structure remains mostly backward compatible. Try not to rename fields, in case someone has written scripts against it.
23 |
24 |
25 | ## Unit tests vs Integration tests
26 | For the purposes of this project, "integration tests" use the filesystem. The [official Rust recommendation](https://doc.rust-lang.org/book/ch11-03-test-organization.html)
27 | is:
28 |
29 | * **Unit tests** go inline inside source files, in a `#[cfg(test)]` module. Structure your code so that
30 | you can use these to test private functions without using the external dependencies like the
31 | filesystem.
32 | * **Integration tests** go "externally", in the `/tests` folder. Use the utilities in `tests/util` to
33 | work with external dependencies easier.
34 | * `git_repo` — makes it easy to work with Git repositories in a temp directory. It does it in a way
35 | that tests can continue to run in parallel without interfering with each other.
36 | * `dura` — makes it easy to call the real `dura` executable in a sub-process. This makes it
37 | possible to run tests in parallel by setting environment varibales only for the sub-process
38 | (e.g. `$DURA_HOME`). It also uses the `util::daemon` module to facilitate working with `dura serve`
39 | by allowing you to make a blocking call to `read_line` to wait the minimum amount of time for
40 | an activity to happen (like startup or snapshots).
41 |
42 |
43 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "adler"
7 | version = "1.0.2"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
10 |
11 | [[package]]
12 | name = "ansi_term"
13 | version = "0.12.1"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
16 | dependencies = [
17 | "winapi",
18 | ]
19 |
20 | [[package]]
21 | name = "anyhow"
22 | version = "1.0.66"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
25 |
26 | [[package]]
27 | name = "autocfg"
28 | version = "1.1.0"
29 | source = "registry+https://github.com/rust-lang/crates.io-index"
30 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
31 |
32 | [[package]]
33 | name = "base64"
34 | version = "0.13.1"
35 | source = "registry+https://github.com/rust-lang/crates.io-index"
36 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
37 |
38 | [[package]]
39 | name = "bitflags"
40 | version = "1.3.2"
41 | source = "registry+https://github.com/rust-lang/crates.io-index"
42 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
43 |
44 | [[package]]
45 | name = "byteorder"
46 | version = "1.4.3"
47 | source = "registry+https://github.com/rust-lang/crates.io-index"
48 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
49 |
50 | [[package]]
51 | name = "bytes"
52 | version = "1.1.0"
53 | source = "registry+https://github.com/rust-lang/crates.io-index"
54 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
55 |
56 | [[package]]
57 | name = "cc"
58 | version = "1.0.72"
59 | source = "registry+https://github.com/rust-lang/crates.io-index"
60 | checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
61 | dependencies = [
62 | "jobserver",
63 | ]
64 |
65 | [[package]]
66 | name = "cfg-if"
67 | version = "1.0.0"
68 | source = "registry+https://github.com/rust-lang/crates.io-index"
69 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
70 |
71 | [[package]]
72 | name = "chrono"
73 | version = "0.4.19"
74 | source = "registry+https://github.com/rust-lang/crates.io-index"
75 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
76 | dependencies = [
77 | "libc",
78 | "num-integer",
79 | "num-traits",
80 | "time",
81 | "winapi",
82 | ]
83 |
84 | [[package]]
85 | name = "clap"
86 | version = "4.0.27"
87 | source = "registry+https://github.com/rust-lang/crates.io-index"
88 | checksum = "0acbd8d28a0a60d7108d7ae850af6ba34cf2d1257fc646980e5f97ce14275966"
89 | dependencies = [
90 | "bitflags",
91 | "clap_lex",
92 | "is-terminal",
93 | "once_cell",
94 | "strsim",
95 | "termcolor",
96 | ]
97 |
98 | [[package]]
99 | name = "clap_lex"
100 | version = "0.3.0"
101 | source = "registry+https://github.com/rust-lang/crates.io-index"
102 | checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
103 | dependencies = [
104 | "os_str_bytes",
105 | ]
106 |
107 | [[package]]
108 | name = "crc32fast"
109 | version = "1.3.2"
110 | source = "registry+https://github.com/rust-lang/crates.io-index"
111 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
112 | dependencies = [
113 | "cfg-if",
114 | ]
115 |
116 | [[package]]
117 | name = "crossbeam-channel"
118 | version = "0.5.6"
119 | source = "registry+https://github.com/rust-lang/crates.io-index"
120 | checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
121 | dependencies = [
122 | "cfg-if",
123 | "crossbeam-utils",
124 | ]
125 |
126 | [[package]]
127 | name = "crossbeam-utils"
128 | version = "0.8.12"
129 | source = "registry+https://github.com/rust-lang/crates.io-index"
130 | checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
131 | dependencies = [
132 | "cfg-if",
133 | ]
134 |
135 | [[package]]
136 | name = "dashmap"
137 | version = "5.4.0"
138 | source = "registry+https://github.com/rust-lang/crates.io-index"
139 | checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
140 | dependencies = [
141 | "cfg-if",
142 | "hashbrown",
143 | "lock_api",
144 | "once_cell",
145 | "parking_lot_core 0.9.4",
146 | ]
147 |
148 | [[package]]
149 | name = "dirs"
150 | version = "4.0.0"
151 | source = "registry+https://github.com/rust-lang/crates.io-index"
152 | checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
153 | dependencies = [
154 | "dirs-sys",
155 | ]
156 |
157 | [[package]]
158 | name = "dirs-sys"
159 | version = "0.3.6"
160 | source = "registry+https://github.com/rust-lang/crates.io-index"
161 | checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
162 | dependencies = [
163 | "libc",
164 | "redox_users",
165 | "winapi",
166 | ]
167 |
168 | [[package]]
169 | name = "dura"
170 | version = "0.2.0-dev"
171 | dependencies = [
172 | "anyhow",
173 | "chrono",
174 | "clap",
175 | "dirs",
176 | "git2",
177 | "hdrhistogram",
178 | "serde",
179 | "serde_json",
180 | "serial_test",
181 | "sudo",
182 | "tempfile",
183 | "tokio",
184 | "toml",
185 | "tracing",
186 | "tracing-subscriber",
187 | "walkdir",
188 | ]
189 |
190 | [[package]]
191 | name = "errno"
192 | version = "0.2.8"
193 | source = "registry+https://github.com/rust-lang/crates.io-index"
194 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
195 | dependencies = [
196 | "errno-dragonfly",
197 | "libc",
198 | "winapi",
199 | ]
200 |
201 | [[package]]
202 | name = "errno-dragonfly"
203 | version = "0.1.2"
204 | source = "registry+https://github.com/rust-lang/crates.io-index"
205 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
206 | dependencies = [
207 | "cc",
208 | "libc",
209 | ]
210 |
211 | [[package]]
212 | name = "fastrand"
213 | version = "1.6.0"
214 | source = "registry+https://github.com/rust-lang/crates.io-index"
215 | checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2"
216 | dependencies = [
217 | "instant",
218 | ]
219 |
220 | [[package]]
221 | name = "flate2"
222 | version = "1.0.24"
223 | source = "registry+https://github.com/rust-lang/crates.io-index"
224 | checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
225 | dependencies = [
226 | "crc32fast",
227 | "miniz_oxide",
228 | ]
229 |
230 | [[package]]
231 | name = "form_urlencoded"
232 | version = "1.0.1"
233 | source = "registry+https://github.com/rust-lang/crates.io-index"
234 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
235 | dependencies = [
236 | "matches",
237 | "percent-encoding",
238 | ]
239 |
240 | [[package]]
241 | name = "futures"
242 | version = "0.3.25"
243 | source = "registry+https://github.com/rust-lang/crates.io-index"
244 | checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
245 | dependencies = [
246 | "futures-channel",
247 | "futures-core",
248 | "futures-executor",
249 | "futures-io",
250 | "futures-sink",
251 | "futures-task",
252 | "futures-util",
253 | ]
254 |
255 | [[package]]
256 | name = "futures-channel"
257 | version = "0.3.25"
258 | source = "registry+https://github.com/rust-lang/crates.io-index"
259 | checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
260 | dependencies = [
261 | "futures-core",
262 | "futures-sink",
263 | ]
264 |
265 | [[package]]
266 | name = "futures-core"
267 | version = "0.3.25"
268 | source = "registry+https://github.com/rust-lang/crates.io-index"
269 | checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
270 |
271 | [[package]]
272 | name = "futures-executor"
273 | version = "0.3.25"
274 | source = "registry+https://github.com/rust-lang/crates.io-index"
275 | checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
276 | dependencies = [
277 | "futures-core",
278 | "futures-task",
279 | "futures-util",
280 | ]
281 |
282 | [[package]]
283 | name = "futures-io"
284 | version = "0.3.25"
285 | source = "registry+https://github.com/rust-lang/crates.io-index"
286 | checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
287 |
288 | [[package]]
289 | name = "futures-sink"
290 | version = "0.3.25"
291 | source = "registry+https://github.com/rust-lang/crates.io-index"
292 | checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
293 |
294 | [[package]]
295 | name = "futures-task"
296 | version = "0.3.25"
297 | source = "registry+https://github.com/rust-lang/crates.io-index"
298 | checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
299 |
300 | [[package]]
301 | name = "futures-util"
302 | version = "0.3.25"
303 | source = "registry+https://github.com/rust-lang/crates.io-index"
304 | checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
305 | dependencies = [
306 | "futures-channel",
307 | "futures-core",
308 | "futures-io",
309 | "futures-sink",
310 | "futures-task",
311 | "memchr",
312 | "pin-project-lite",
313 | "pin-utils",
314 | "slab",
315 | ]
316 |
317 | [[package]]
318 | name = "getrandom"
319 | version = "0.2.3"
320 | source = "registry+https://github.com/rust-lang/crates.io-index"
321 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
322 | dependencies = [
323 | "cfg-if",
324 | "libc",
325 | "wasi",
326 | ]
327 |
328 | [[package]]
329 | name = "git2"
330 | version = "0.15.0"
331 | source = "registry+https://github.com/rust-lang/crates.io-index"
332 | checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1"
333 | dependencies = [
334 | "bitflags",
335 | "libc",
336 | "libgit2-sys",
337 | "log",
338 | "openssl-probe",
339 | "openssl-sys",
340 | "url",
341 | ]
342 |
343 | [[package]]
344 | name = "hashbrown"
345 | version = "0.12.3"
346 | source = "registry+https://github.com/rust-lang/crates.io-index"
347 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
348 |
349 | [[package]]
350 | name = "hdrhistogram"
351 | version = "7.5.2"
352 | source = "registry+https://github.com/rust-lang/crates.io-index"
353 | checksum = "7f19b9f54f7c7f55e31401bb647626ce0cf0f67b0004982ce815b3ee72a02aa8"
354 | dependencies = [
355 | "base64",
356 | "byteorder",
357 | "crossbeam-channel",
358 | "flate2",
359 | "nom",
360 | "num-traits",
361 | ]
362 |
363 | [[package]]
364 | name = "hermit-abi"
365 | version = "0.1.19"
366 | source = "registry+https://github.com/rust-lang/crates.io-index"
367 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
368 | dependencies = [
369 | "libc",
370 | ]
371 |
372 | [[package]]
373 | name = "hermit-abi"
374 | version = "0.2.6"
375 | source = "registry+https://github.com/rust-lang/crates.io-index"
376 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
377 | dependencies = [
378 | "libc",
379 | ]
380 |
381 | [[package]]
382 | name = "idna"
383 | version = "0.2.3"
384 | source = "registry+https://github.com/rust-lang/crates.io-index"
385 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
386 | dependencies = [
387 | "matches",
388 | "unicode-bidi",
389 | "unicode-normalization",
390 | ]
391 |
392 | [[package]]
393 | name = "instant"
394 | version = "0.1.12"
395 | source = "registry+https://github.com/rust-lang/crates.io-index"
396 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
397 | dependencies = [
398 | "cfg-if",
399 | ]
400 |
401 | [[package]]
402 | name = "io-lifetimes"
403 | version = "1.0.3"
404 | source = "registry+https://github.com/rust-lang/crates.io-index"
405 | checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c"
406 | dependencies = [
407 | "libc",
408 | "windows-sys",
409 | ]
410 |
411 | [[package]]
412 | name = "is-terminal"
413 | version = "0.4.0"
414 | source = "registry+https://github.com/rust-lang/crates.io-index"
415 | checksum = "aae5bc6e2eb41c9def29a3e0f1306382807764b9b53112030eff57435667352d"
416 | dependencies = [
417 | "hermit-abi 0.2.6",
418 | "io-lifetimes",
419 | "rustix",
420 | "windows-sys",
421 | ]
422 |
423 | [[package]]
424 | name = "itoa"
425 | version = "1.0.1"
426 | source = "registry+https://github.com/rust-lang/crates.io-index"
427 | checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
428 |
429 | [[package]]
430 | name = "jobserver"
431 | version = "0.1.24"
432 | source = "registry+https://github.com/rust-lang/crates.io-index"
433 | checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa"
434 | dependencies = [
435 | "libc",
436 | ]
437 |
438 | [[package]]
439 | name = "lazy_static"
440 | version = "1.4.0"
441 | source = "registry+https://github.com/rust-lang/crates.io-index"
442 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
443 |
444 | [[package]]
445 | name = "libc"
446 | version = "0.2.137"
447 | source = "registry+https://github.com/rust-lang/crates.io-index"
448 | checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
449 |
450 | [[package]]
451 | name = "libgit2-sys"
452 | version = "0.14.0+1.5.0"
453 | source = "registry+https://github.com/rust-lang/crates.io-index"
454 | checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b"
455 | dependencies = [
456 | "cc",
457 | "libc",
458 | "libssh2-sys",
459 | "libz-sys",
460 | "openssl-sys",
461 | "pkg-config",
462 | ]
463 |
464 | [[package]]
465 | name = "libssh2-sys"
466 | version = "0.2.23"
467 | source = "registry+https://github.com/rust-lang/crates.io-index"
468 | checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca"
469 | dependencies = [
470 | "cc",
471 | "libc",
472 | "libz-sys",
473 | "openssl-sys",
474 | "pkg-config",
475 | "vcpkg",
476 | ]
477 |
478 | [[package]]
479 | name = "libz-sys"
480 | version = "1.1.3"
481 | source = "registry+https://github.com/rust-lang/crates.io-index"
482 | checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66"
483 | dependencies = [
484 | "cc",
485 | "libc",
486 | "pkg-config",
487 | "vcpkg",
488 | ]
489 |
490 | [[package]]
491 | name = "linux-raw-sys"
492 | version = "0.1.3"
493 | source = "registry+https://github.com/rust-lang/crates.io-index"
494 | checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f"
495 |
496 | [[package]]
497 | name = "lock_api"
498 | version = "0.4.9"
499 | source = "registry+https://github.com/rust-lang/crates.io-index"
500 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
501 | dependencies = [
502 | "autocfg",
503 | "scopeguard",
504 | ]
505 |
506 | [[package]]
507 | name = "log"
508 | version = "0.4.14"
509 | source = "registry+https://github.com/rust-lang/crates.io-index"
510 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
511 | dependencies = [
512 | "cfg-if",
513 | ]
514 |
515 | [[package]]
516 | name = "matchers"
517 | version = "0.1.0"
518 | source = "registry+https://github.com/rust-lang/crates.io-index"
519 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
520 | dependencies = [
521 | "regex-automata",
522 | ]
523 |
524 | [[package]]
525 | name = "matches"
526 | version = "0.1.9"
527 | source = "registry+https://github.com/rust-lang/crates.io-index"
528 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
529 |
530 | [[package]]
531 | name = "memchr"
532 | version = "2.4.1"
533 | source = "registry+https://github.com/rust-lang/crates.io-index"
534 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
535 |
536 | [[package]]
537 | name = "minimal-lexical"
538 | version = "0.2.1"
539 | source = "registry+https://github.com/rust-lang/crates.io-index"
540 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
541 |
542 | [[package]]
543 | name = "miniz_oxide"
544 | version = "0.5.4"
545 | source = "registry+https://github.com/rust-lang/crates.io-index"
546 | checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
547 | dependencies = [
548 | "adler",
549 | ]
550 |
551 | [[package]]
552 | name = "mio"
553 | version = "0.7.14"
554 | source = "registry+https://github.com/rust-lang/crates.io-index"
555 | checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
556 | dependencies = [
557 | "libc",
558 | "log",
559 | "miow",
560 | "ntapi",
561 | "winapi",
562 | ]
563 |
564 | [[package]]
565 | name = "miow"
566 | version = "0.3.7"
567 | source = "registry+https://github.com/rust-lang/crates.io-index"
568 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
569 | dependencies = [
570 | "winapi",
571 | ]
572 |
573 | [[package]]
574 | name = "nom"
575 | version = "7.1.1"
576 | source = "registry+https://github.com/rust-lang/crates.io-index"
577 | checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
578 | dependencies = [
579 | "memchr",
580 | "minimal-lexical",
581 | ]
582 |
583 | [[package]]
584 | name = "ntapi"
585 | version = "0.3.6"
586 | source = "registry+https://github.com/rust-lang/crates.io-index"
587 | checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
588 | dependencies = [
589 | "winapi",
590 | ]
591 |
592 | [[package]]
593 | name = "num-integer"
594 | version = "0.1.44"
595 | source = "registry+https://github.com/rust-lang/crates.io-index"
596 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
597 | dependencies = [
598 | "autocfg",
599 | "num-traits",
600 | ]
601 |
602 | [[package]]
603 | name = "num-traits"
604 | version = "0.2.14"
605 | source = "registry+https://github.com/rust-lang/crates.io-index"
606 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
607 | dependencies = [
608 | "autocfg",
609 | ]
610 |
611 | [[package]]
612 | name = "num_cpus"
613 | version = "1.13.1"
614 | source = "registry+https://github.com/rust-lang/crates.io-index"
615 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
616 | dependencies = [
617 | "hermit-abi 0.1.19",
618 | "libc",
619 | ]
620 |
621 | [[package]]
622 | name = "once_cell"
623 | version = "1.16.0"
624 | source = "registry+https://github.com/rust-lang/crates.io-index"
625 | checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
626 |
627 | [[package]]
628 | name = "openssl-probe"
629 | version = "0.1.4"
630 | source = "registry+https://github.com/rust-lang/crates.io-index"
631 | checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
632 |
633 | [[package]]
634 | name = "openssl-sys"
635 | version = "0.9.72"
636 | source = "registry+https://github.com/rust-lang/crates.io-index"
637 | checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"
638 | dependencies = [
639 | "autocfg",
640 | "cc",
641 | "libc",
642 | "pkg-config",
643 | "vcpkg",
644 | ]
645 |
646 | [[package]]
647 | name = "os_str_bytes"
648 | version = "6.0.0"
649 | source = "registry+https://github.com/rust-lang/crates.io-index"
650 | checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
651 |
652 | [[package]]
653 | name = "parking_lot"
654 | version = "0.11.2"
655 | source = "registry+https://github.com/rust-lang/crates.io-index"
656 | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
657 | dependencies = [
658 | "instant",
659 | "lock_api",
660 | "parking_lot_core 0.8.5",
661 | ]
662 |
663 | [[package]]
664 | name = "parking_lot"
665 | version = "0.12.1"
666 | source = "registry+https://github.com/rust-lang/crates.io-index"
667 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
668 | dependencies = [
669 | "lock_api",
670 | "parking_lot_core 0.9.4",
671 | ]
672 |
673 | [[package]]
674 | name = "parking_lot_core"
675 | version = "0.8.5"
676 | source = "registry+https://github.com/rust-lang/crates.io-index"
677 | checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
678 | dependencies = [
679 | "cfg-if",
680 | "instant",
681 | "libc",
682 | "redox_syscall",
683 | "smallvec",
684 | "winapi",
685 | ]
686 |
687 | [[package]]
688 | name = "parking_lot_core"
689 | version = "0.9.4"
690 | source = "registry+https://github.com/rust-lang/crates.io-index"
691 | checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0"
692 | dependencies = [
693 | "cfg-if",
694 | "libc",
695 | "redox_syscall",
696 | "smallvec",
697 | "windows-sys",
698 | ]
699 |
700 | [[package]]
701 | name = "percent-encoding"
702 | version = "2.1.0"
703 | source = "registry+https://github.com/rust-lang/crates.io-index"
704 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
705 |
706 | [[package]]
707 | name = "pin-project-lite"
708 | version = "0.2.8"
709 | source = "registry+https://github.com/rust-lang/crates.io-index"
710 | checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
711 |
712 | [[package]]
713 | name = "pin-utils"
714 | version = "0.1.0"
715 | source = "registry+https://github.com/rust-lang/crates.io-index"
716 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
717 |
718 | [[package]]
719 | name = "pkg-config"
720 | version = "0.3.24"
721 | source = "registry+https://github.com/rust-lang/crates.io-index"
722 | checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
723 |
724 | [[package]]
725 | name = "proc-macro-error"
726 | version = "1.0.4"
727 | source = "registry+https://github.com/rust-lang/crates.io-index"
728 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
729 | dependencies = [
730 | "proc-macro-error-attr",
731 | "proc-macro2",
732 | "quote",
733 | "syn",
734 | "version_check",
735 | ]
736 |
737 | [[package]]
738 | name = "proc-macro-error-attr"
739 | version = "1.0.4"
740 | source = "registry+https://github.com/rust-lang/crates.io-index"
741 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
742 | dependencies = [
743 | "proc-macro2",
744 | "quote",
745 | "version_check",
746 | ]
747 |
748 | [[package]]
749 | name = "proc-macro2"
750 | version = "1.0.36"
751 | source = "registry+https://github.com/rust-lang/crates.io-index"
752 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
753 | dependencies = [
754 | "unicode-xid",
755 | ]
756 |
757 | [[package]]
758 | name = "quote"
759 | version = "1.0.14"
760 | source = "registry+https://github.com/rust-lang/crates.io-index"
761 | checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d"
762 | dependencies = [
763 | "proc-macro2",
764 | ]
765 |
766 | [[package]]
767 | name = "redox_syscall"
768 | version = "0.2.10"
769 | source = "registry+https://github.com/rust-lang/crates.io-index"
770 | checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
771 | dependencies = [
772 | "bitflags",
773 | ]
774 |
775 | [[package]]
776 | name = "redox_users"
777 | version = "0.4.0"
778 | source = "registry+https://github.com/rust-lang/crates.io-index"
779 | checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
780 | dependencies = [
781 | "getrandom",
782 | "redox_syscall",
783 | ]
784 |
785 | [[package]]
786 | name = "regex"
787 | version = "1.5.5"
788 | source = "registry+https://github.com/rust-lang/crates.io-index"
789 | checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
790 | dependencies = [
791 | "regex-syntax",
792 | ]
793 |
794 | [[package]]
795 | name = "regex-automata"
796 | version = "0.1.10"
797 | source = "registry+https://github.com/rust-lang/crates.io-index"
798 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
799 | dependencies = [
800 | "regex-syntax",
801 | ]
802 |
803 | [[package]]
804 | name = "regex-syntax"
805 | version = "0.6.25"
806 | source = "registry+https://github.com/rust-lang/crates.io-index"
807 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
808 |
809 | [[package]]
810 | name = "remove_dir_all"
811 | version = "0.5.3"
812 | source = "registry+https://github.com/rust-lang/crates.io-index"
813 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
814 | dependencies = [
815 | "winapi",
816 | ]
817 |
818 | [[package]]
819 | name = "rustix"
820 | version = "0.36.3"
821 | source = "registry+https://github.com/rust-lang/crates.io-index"
822 | checksum = "0b1fbb4dfc4eb1d390c02df47760bb19a84bb80b301ecc947ab5406394d8223e"
823 | dependencies = [
824 | "bitflags",
825 | "errno",
826 | "io-lifetimes",
827 | "libc",
828 | "linux-raw-sys",
829 | "windows-sys",
830 | ]
831 |
832 | [[package]]
833 | name = "ryu"
834 | version = "1.0.9"
835 | source = "registry+https://github.com/rust-lang/crates.io-index"
836 | checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
837 |
838 | [[package]]
839 | name = "same-file"
840 | version = "1.0.6"
841 | source = "registry+https://github.com/rust-lang/crates.io-index"
842 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
843 | dependencies = [
844 | "winapi-util",
845 | ]
846 |
847 | [[package]]
848 | name = "scopeguard"
849 | version = "1.1.0"
850 | source = "registry+https://github.com/rust-lang/crates.io-index"
851 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
852 |
853 | [[package]]
854 | name = "serde"
855 | version = "1.0.133"
856 | source = "registry+https://github.com/rust-lang/crates.io-index"
857 | checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a"
858 | dependencies = [
859 | "serde_derive",
860 | ]
861 |
862 | [[package]]
863 | name = "serde_derive"
864 | version = "1.0.133"
865 | source = "registry+https://github.com/rust-lang/crates.io-index"
866 | checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537"
867 | dependencies = [
868 | "proc-macro2",
869 | "quote",
870 | "syn",
871 | ]
872 |
873 | [[package]]
874 | name = "serde_json"
875 | version = "1.0.74"
876 | source = "registry+https://github.com/rust-lang/crates.io-index"
877 | checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142"
878 | dependencies = [
879 | "itoa",
880 | "ryu",
881 | "serde",
882 | ]
883 |
884 | [[package]]
885 | name = "serial_test"
886 | version = "0.9.0"
887 | source = "registry+https://github.com/rust-lang/crates.io-index"
888 | checksum = "92761393ee4dc3ff8f4af487bd58f4307c9329bbedea02cac0089ad9c411e153"
889 | dependencies = [
890 | "dashmap",
891 | "futures",
892 | "lazy_static",
893 | "log",
894 | "parking_lot 0.12.1",
895 | "serial_test_derive",
896 | ]
897 |
898 | [[package]]
899 | name = "serial_test_derive"
900 | version = "0.9.0"
901 | source = "registry+https://github.com/rust-lang/crates.io-index"
902 | checksum = "4b6f5d1c3087fb119617cff2966fe3808a80e5eb59a8c1601d5994d66f4346a5"
903 | dependencies = [
904 | "proc-macro-error",
905 | "proc-macro2",
906 | "quote",
907 | "syn",
908 | ]
909 |
910 | [[package]]
911 | name = "sharded-slab"
912 | version = "0.1.4"
913 | source = "registry+https://github.com/rust-lang/crates.io-index"
914 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
915 | dependencies = [
916 | "lazy_static",
917 | ]
918 |
919 | [[package]]
920 | name = "signal-hook-registry"
921 | version = "1.4.0"
922 | source = "registry+https://github.com/rust-lang/crates.io-index"
923 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
924 | dependencies = [
925 | "libc",
926 | ]
927 |
928 | [[package]]
929 | name = "slab"
930 | version = "0.4.7"
931 | source = "registry+https://github.com/rust-lang/crates.io-index"
932 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
933 | dependencies = [
934 | "autocfg",
935 | ]
936 |
937 | [[package]]
938 | name = "smallvec"
939 | version = "1.7.0"
940 | source = "registry+https://github.com/rust-lang/crates.io-index"
941 | checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
942 |
943 | [[package]]
944 | name = "strsim"
945 | version = "0.10.0"
946 | source = "registry+https://github.com/rust-lang/crates.io-index"
947 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
948 |
949 | [[package]]
950 | name = "sudo"
951 | version = "0.6.0"
952 | source = "registry+https://github.com/rust-lang/crates.io-index"
953 | checksum = "88bd84d4c082e18e37fef52c0088e4407dabcef19d23a607fb4b5ee03b7d5b83"
954 | dependencies = [
955 | "libc",
956 | "log",
957 | ]
958 |
959 | [[package]]
960 | name = "syn"
961 | version = "1.0.85"
962 | source = "registry+https://github.com/rust-lang/crates.io-index"
963 | checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7"
964 | dependencies = [
965 | "proc-macro2",
966 | "quote",
967 | "unicode-xid",
968 | ]
969 |
970 | [[package]]
971 | name = "tempfile"
972 | version = "3.3.0"
973 | source = "registry+https://github.com/rust-lang/crates.io-index"
974 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
975 | dependencies = [
976 | "cfg-if",
977 | "fastrand",
978 | "libc",
979 | "redox_syscall",
980 | "remove_dir_all",
981 | "winapi",
982 | ]
983 |
984 | [[package]]
985 | name = "termcolor"
986 | version = "1.1.2"
987 | source = "registry+https://github.com/rust-lang/crates.io-index"
988 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
989 | dependencies = [
990 | "winapi-util",
991 | ]
992 |
993 | [[package]]
994 | name = "thread_local"
995 | version = "1.1.4"
996 | source = "registry+https://github.com/rust-lang/crates.io-index"
997 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
998 | dependencies = [
999 | "once_cell",
1000 | ]
1001 |
1002 | [[package]]
1003 | name = "time"
1004 | version = "0.1.44"
1005 | source = "registry+https://github.com/rust-lang/crates.io-index"
1006 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
1007 | dependencies = [
1008 | "libc",
1009 | "wasi",
1010 | "winapi",
1011 | ]
1012 |
1013 | [[package]]
1014 | name = "tinyvec"
1015 | version = "1.5.1"
1016 | source = "registry+https://github.com/rust-lang/crates.io-index"
1017 | checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
1018 | dependencies = [
1019 | "tinyvec_macros",
1020 | ]
1021 |
1022 | [[package]]
1023 | name = "tinyvec_macros"
1024 | version = "0.1.0"
1025 | source = "registry+https://github.com/rust-lang/crates.io-index"
1026 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
1027 |
1028 | [[package]]
1029 | name = "tokio"
1030 | version = "1.15.0"
1031 | source = "registry+https://github.com/rust-lang/crates.io-index"
1032 | checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838"
1033 | dependencies = [
1034 | "bytes",
1035 | "libc",
1036 | "memchr",
1037 | "mio",
1038 | "num_cpus",
1039 | "once_cell",
1040 | "parking_lot 0.11.2",
1041 | "pin-project-lite",
1042 | "signal-hook-registry",
1043 | "tokio-macros",
1044 | "winapi",
1045 | ]
1046 |
1047 | [[package]]
1048 | name = "tokio-macros"
1049 | version = "1.7.0"
1050 | source = "registry+https://github.com/rust-lang/crates.io-index"
1051 | checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
1052 | dependencies = [
1053 | "proc-macro2",
1054 | "quote",
1055 | "syn",
1056 | ]
1057 |
1058 | [[package]]
1059 | name = "toml"
1060 | version = "0.5.8"
1061 | source = "registry+https://github.com/rust-lang/crates.io-index"
1062 | checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
1063 | dependencies = [
1064 | "serde",
1065 | ]
1066 |
1067 | [[package]]
1068 | name = "tracing"
1069 | version = "0.1.29"
1070 | source = "registry+https://github.com/rust-lang/crates.io-index"
1071 | checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
1072 | dependencies = [
1073 | "cfg-if",
1074 | "pin-project-lite",
1075 | "tracing-attributes",
1076 | "tracing-core",
1077 | ]
1078 |
1079 | [[package]]
1080 | name = "tracing-attributes"
1081 | version = "0.1.18"
1082 | source = "registry+https://github.com/rust-lang/crates.io-index"
1083 | checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e"
1084 | dependencies = [
1085 | "proc-macro2",
1086 | "quote",
1087 | "syn",
1088 | ]
1089 |
1090 | [[package]]
1091 | name = "tracing-core"
1092 | version = "0.1.21"
1093 | source = "registry+https://github.com/rust-lang/crates.io-index"
1094 | checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
1095 | dependencies = [
1096 | "lazy_static",
1097 | ]
1098 |
1099 | [[package]]
1100 | name = "tracing-log"
1101 | version = "0.1.2"
1102 | source = "registry+https://github.com/rust-lang/crates.io-index"
1103 | checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3"
1104 | dependencies = [
1105 | "lazy_static",
1106 | "log",
1107 | "tracing-core",
1108 | ]
1109 |
1110 | [[package]]
1111 | name = "tracing-subscriber"
1112 | version = "0.3.5"
1113 | source = "registry+https://github.com/rust-lang/crates.io-index"
1114 | checksum = "5d81bfa81424cc98cb034b837c985b7a290f592e5b4322f353f94a0ab0f9f594"
1115 | dependencies = [
1116 | "ansi_term",
1117 | "lazy_static",
1118 | "matchers",
1119 | "regex",
1120 | "sharded-slab",
1121 | "smallvec",
1122 | "thread_local",
1123 | "tracing",
1124 | "tracing-core",
1125 | "tracing-log",
1126 | ]
1127 |
1128 | [[package]]
1129 | name = "unicode-bidi"
1130 | version = "0.3.7"
1131 | source = "registry+https://github.com/rust-lang/crates.io-index"
1132 | checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
1133 |
1134 | [[package]]
1135 | name = "unicode-normalization"
1136 | version = "0.1.19"
1137 | source = "registry+https://github.com/rust-lang/crates.io-index"
1138 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
1139 | dependencies = [
1140 | "tinyvec",
1141 | ]
1142 |
1143 | [[package]]
1144 | name = "unicode-xid"
1145 | version = "0.2.2"
1146 | source = "registry+https://github.com/rust-lang/crates.io-index"
1147 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
1148 |
1149 | [[package]]
1150 | name = "url"
1151 | version = "2.2.2"
1152 | source = "registry+https://github.com/rust-lang/crates.io-index"
1153 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
1154 | dependencies = [
1155 | "form_urlencoded",
1156 | "idna",
1157 | "matches",
1158 | "percent-encoding",
1159 | ]
1160 |
1161 | [[package]]
1162 | name = "vcpkg"
1163 | version = "0.2.15"
1164 | source = "registry+https://github.com/rust-lang/crates.io-index"
1165 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
1166 |
1167 | [[package]]
1168 | name = "version_check"
1169 | version = "0.9.4"
1170 | source = "registry+https://github.com/rust-lang/crates.io-index"
1171 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
1172 |
1173 | [[package]]
1174 | name = "walkdir"
1175 | version = "2.3.2"
1176 | source = "registry+https://github.com/rust-lang/crates.io-index"
1177 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
1178 | dependencies = [
1179 | "same-file",
1180 | "winapi",
1181 | "winapi-util",
1182 | ]
1183 |
1184 | [[package]]
1185 | name = "wasi"
1186 | version = "0.10.0+wasi-snapshot-preview1"
1187 | source = "registry+https://github.com/rust-lang/crates.io-index"
1188 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
1189 |
1190 | [[package]]
1191 | name = "winapi"
1192 | version = "0.3.9"
1193 | source = "registry+https://github.com/rust-lang/crates.io-index"
1194 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
1195 | dependencies = [
1196 | "winapi-i686-pc-windows-gnu",
1197 | "winapi-x86_64-pc-windows-gnu",
1198 | ]
1199 |
1200 | [[package]]
1201 | name = "winapi-i686-pc-windows-gnu"
1202 | version = "0.4.0"
1203 | source = "registry+https://github.com/rust-lang/crates.io-index"
1204 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1205 |
1206 | [[package]]
1207 | name = "winapi-util"
1208 | version = "0.1.5"
1209 | source = "registry+https://github.com/rust-lang/crates.io-index"
1210 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
1211 | dependencies = [
1212 | "winapi",
1213 | ]
1214 |
1215 | [[package]]
1216 | name = "winapi-x86_64-pc-windows-gnu"
1217 | version = "0.4.0"
1218 | source = "registry+https://github.com/rust-lang/crates.io-index"
1219 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1220 |
1221 | [[package]]
1222 | name = "windows-sys"
1223 | version = "0.42.0"
1224 | source = "registry+https://github.com/rust-lang/crates.io-index"
1225 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
1226 | dependencies = [
1227 | "windows_aarch64_gnullvm",
1228 | "windows_aarch64_msvc",
1229 | "windows_i686_gnu",
1230 | "windows_i686_msvc",
1231 | "windows_x86_64_gnu",
1232 | "windows_x86_64_gnullvm",
1233 | "windows_x86_64_msvc",
1234 | ]
1235 |
1236 | [[package]]
1237 | name = "windows_aarch64_gnullvm"
1238 | version = "0.42.0"
1239 | source = "registry+https://github.com/rust-lang/crates.io-index"
1240 | checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
1241 |
1242 | [[package]]
1243 | name = "windows_aarch64_msvc"
1244 | version = "0.42.0"
1245 | source = "registry+https://github.com/rust-lang/crates.io-index"
1246 | checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
1247 |
1248 | [[package]]
1249 | name = "windows_i686_gnu"
1250 | version = "0.42.0"
1251 | source = "registry+https://github.com/rust-lang/crates.io-index"
1252 | checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
1253 |
1254 | [[package]]
1255 | name = "windows_i686_msvc"
1256 | version = "0.42.0"
1257 | source = "registry+https://github.com/rust-lang/crates.io-index"
1258 | checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
1259 |
1260 | [[package]]
1261 | name = "windows_x86_64_gnu"
1262 | version = "0.42.0"
1263 | source = "registry+https://github.com/rust-lang/crates.io-index"
1264 | checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
1265 |
1266 | [[package]]
1267 | name = "windows_x86_64_gnullvm"
1268 | version = "0.42.0"
1269 | source = "registry+https://github.com/rust-lang/crates.io-index"
1270 | checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
1271 |
1272 | [[package]]
1273 | name = "windows_x86_64_msvc"
1274 | version = "0.42.0"
1275 | source = "registry+https://github.com/rust-lang/crates.io-index"
1276 | checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
1277 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "dura"
3 | version = "0.2.0-dev"
4 | edition = "2021"
5 | authors = ["Tim Kellogg and the Internet"]
6 | description = "Dura backs up your work automatically via Git commits."
7 | license = "Apache-2.0"
8 | homepage = "https://github.com/tkellogg/dura/"
9 | repository = "https://github.com/tkellogg/dura/"
10 | documentation = "https://github.com/tkellogg/dura/blob/master/README.md"
11 |
12 | [dependencies]
13 | anyhow = "1.0.66"
14 | clap = { version = "4.0", features = ["cargo", "string"] }
15 | git2 = "0.15"
16 | hdrhistogram = "7.5.2"
17 | dirs = "4.0.0"
18 | tokio = { version = "1", features = ["full"] }
19 | serde = { version = "1.0", features = ["derive", "rc"] }
20 | serde_json = "1.0"
21 | chrono = "0.4"
22 | toml = "0.5.8"
23 | tracing = { version = "0.1.5"}
24 | tracing-subscriber = { version = "0.3", features = ["env-filter", "registry"] }
25 | walkdir = "2.3.2"
26 | sudo = "0.6.0"
27 |
28 | [dev-dependencies]
29 | tempfile = "3.2.0"
30 | serial_test = "0.9.0"
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2022 Tim Kellogg
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dura
2 |
3 | [![Build][build badge]][build action]
4 |
5 | Dura is a background process that watches your Git repositories and commits your uncommitted changes without impacting
6 | HEAD, the current branch, or the Git index (staged files). If you ever get into an "oh snap!" situation where you think
7 | you just lost days of work, checkout a `dura` branch and recover.
8 |
9 | Without `dura`, you use Ctrl-Z in your editor to get back to a good state. That's so 2021. Computers crash and Ctrl-Z
10 | only works on files independently. Dura snapshots changes across the entire repository as-you-go, so you can revert to
11 | "4 hours ago" instead of "hit Ctrl-Z like 40 times or whatever". Finally, some sanity.
12 |
13 | ## How to use
14 |
15 | Run it in the background:
16 |
17 | ```bash
18 | $ dura serve &
19 | ```
20 |
21 | The `serve` can happen in any directory. The `&` is Unix shell syntax to run the process in the background, meaning that you can start
22 | `dura` and then keep using the same terminal window while `dura` keeps running. You could also run `dura serve` in a
23 | window that you keep open.
24 |
25 | Let `dura` know which repositories to watch:
26 |
27 | ```bash
28 | $ cd some/git/repo
29 | $ dura watch
30 | ```
31 |
32 | Right now, you have to `cd` into each repo that you want to watch, one at a time.
33 |
34 | If you have thoughts on how to do this better, share them [here](https://github.com/tkellogg/dura/issues/3). Until that's sorted, you can
35 | run something like `find ~ -type d -name .git -prune | xargs -I= sh -c "cd =/..; dura watch"` to get started on your existing repos.
36 |
37 | Make some changes. No need to commit or even stage them. Use any Git tool to see the `dura` branches:
38 |
39 | ```bash
40 | $ git log --all
41 | ```
42 |
43 | `dura` produces a branch for every real commit you make and makes commits to that branch without impacting your working
44 | copy. You keep using Git exactly as you did before.
45 |
46 |
47 | Let `dura` know that it should stop running in the background with the `kill` command.
48 |
49 | ```bash
50 | $ dura kill
51 | ```
52 |
53 | The `kill` can happen in any directory. It indicates to the `serve`
54 | process that it should exit if there is a `serve` process running.
55 |
56 | ## How to recover
57 |
58 | The `dura` branch that's tracking your current uncommitted changes looks like `dura/f4a88e5ea0f1f7492845f7021ae82db70f14c725`.
59 | In $SHELL, you can get the branch name via:
60 |
61 | ```bash
62 | $ echo "dura/$(git rev-parse HEAD)"
63 | ```
64 |
65 | Use `git log` or [`tig`](https://jonas.github.io/tig/) to figure out which commit you want to rollback to. Copy the hash
66 | and then run something like
67 |
68 | ```bash
69 | # Or, if you don't trust dura yet, `git stash`
70 | $ git reset HEAD --hard
71 | # get the changes into your working directory
72 | $ git checkout $THE_HASH
73 | # last few commands reset HEAD back to master but with changes uncommitted
74 | $ git checkout -b temp-branch
75 | $ git reset master
76 | $ git checkout master
77 | $ git branch -D temp-branch
78 | ```
79 |
80 | If you're interested in improving this experience, [collaborate here](https://github.com/tkellogg/dura/issues/4).
81 |
82 | ## Install
83 |
84 | ### Cargo Install
85 | 1. Install Cargo
86 | 2. If you want run release version, type ```cargo install dura``` else type ```cargo install --git https://github.com/tkellogg/dura```
87 |
88 | ### By Source
89 |
90 | 1. Install Rust (e.g., `brew install rustup && brew install rust`)
91 | 2. Clone this repository (e.g., `git clone https://github.com/tkellogg/dura.git`)
92 | 3. Navigate to repository base directory (`cd dura`)
93 | 4. Run `cargo install --path .` **Note:** If you receive a failure fetching the cargo dependencies try using the local [git client for cargo fetches](https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli). `CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install --path .`
94 |
95 | ### Mac OS X
96 |
97 | This installs `dura` and sets up a launchctl service to keep it running.
98 |
99 | ```bash
100 | $ brew install dura
101 | ```
102 |
103 | ### Windows
104 | 1. Download [rustup-init](https://www.rust-lang.org/tools/install)
105 | 2. Clone this repository (e.g., `git clone https://github.com/tkellogg/dura.git`)
106 | 3. Navigate to repository base directory (`cd dura`)
107 | 4. Run `cargo install --path .` **Note:** If you receive a failure fetching the cargo dependencies try using the local [git client for cargo fetches](https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli). `CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install --path .`
108 |
109 | ### Arch Linux
110 |
111 | ```bash
112 | $ paru -S dura-git
113 | ```
114 |
115 | ### Nix / Nixos
116 |
117 | [Nix][nix website] is a tool that takes a unique approach to package
118 | management and system configuration. NixOS is a Linux distribution
119 | built on top of the Nix package manager.
120 |
121 | To run `dura` locally using pre-compiled binaries:
122 |
123 | ```bash
124 | nix shell nixpkgs#dura
125 | ```
126 |
127 | If you're willing to contribute and develop, `dura` also provides its
128 | own ready-to-use [Nix flake][nix flake].
129 |
130 | To build and run the latest development version of `dura` locally:
131 |
132 | ```bash
133 | nix run github:tkellogg/dura
134 | ```
135 |
136 | To run a development environment with the required tools
137 | to develop:
138 |
139 | ```bash
140 | nix develop github:tkellogg/dura
141 | ```
142 |
143 | ## FAQ
144 |
145 | ### Is this stable?
146 |
147 | Yes. Lots of people have been using it since 2022-01-01 without issue. It uses [libgit2](https://libgit2.org/) to make the commits, so it's fairly battle hardened.
148 |
149 | ### How often does this check for changes?
150 |
151 | Every now and then, like 5 seconds or so. Internally there's a control loop that sleeps 5 seconds between iterations, so it
152 | runs less frequently than every 5 seconds (potentially a lot less frequently, if there's a lot of work to do).
153 |
154 |
155 | Brought to you by Tim Kellogg.
156 |
157 |
158 | [build badge]: https://github.com/tkellogg/dura/actions/workflows/build.yaml/badge.svg
159 | [build action]: https://github.com/tkellogg/dura/actions/workflows/build.yaml
160 | [nix website]: https://nixos.org/
161 | [nix flake]: https://nixos.wiki/wiki/Flakes
162 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "locked": {
5 | "lastModified": 1653893745,
6 | "narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
7 | "owner": "numtide",
8 | "repo": "flake-utils",
9 | "rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
10 | "type": "github"
11 | },
12 | "original": {
13 | "owner": "numtide",
14 | "repo": "flake-utils",
15 | "type": "github"
16 | }
17 | },
18 | "flake-utils_2": {
19 | "locked": {
20 | "lastModified": 1637014545,
21 | "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
22 | "owner": "numtide",
23 | "repo": "flake-utils",
24 | "rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
25 | "type": "github"
26 | },
27 | "original": {
28 | "owner": "numtide",
29 | "repo": "flake-utils",
30 | "type": "github"
31 | }
32 | },
33 | "nixpkgs": {
34 | "locked": {
35 | "lastModified": 1654953433,
36 | "narHash": "sha256-TwEeh4r50NdWHFAHQSyjCk2cZxgwUfcCCAJOhPdXB28=",
37 | "owner": "nixos",
38 | "repo": "nixpkgs",
39 | "rev": "90cd5459a1fd707819b9a3fb9c852beaaac3b79a",
40 | "type": "github"
41 | },
42 | "original": {
43 | "owner": "nixos",
44 | "ref": "nixos-unstable",
45 | "repo": "nixpkgs",
46 | "type": "github"
47 | }
48 | },
49 | "nixpkgs_2": {
50 | "locked": {
51 | "lastModified": 1637453606,
52 | "narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
53 | "owner": "NixOS",
54 | "repo": "nixpkgs",
55 | "rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
56 | "type": "github"
57 | },
58 | "original": {
59 | "owner": "NixOS",
60 | "ref": "nixpkgs-unstable",
61 | "repo": "nixpkgs",
62 | "type": "github"
63 | }
64 | },
65 | "root": {
66 | "inputs": {
67 | "flake-utils": "flake-utils",
68 | "nixpkgs": "nixpkgs",
69 | "rust-overlay": "rust-overlay"
70 | }
71 | },
72 | "rust-overlay": {
73 | "inputs": {
74 | "flake-utils": "flake-utils_2",
75 | "nixpkgs": "nixpkgs_2"
76 | },
77 | "locked": {
78 | "lastModified": 1655002087,
79 | "narHash": "sha256-ApxncWKkIIrckV851+S6Xlw7yO+ymLOp0h7De+frCT8=",
80 | "owner": "oxalica",
81 | "repo": "rust-overlay",
82 | "rev": "e04a88d7f859ae9ec42267866bb68c1a741e6859",
83 | "type": "github"
84 | },
85 | "original": {
86 | "owner": "oxalica",
87 | "repo": "rust-overlay",
88 | "type": "github"
89 | }
90 | }
91 | },
92 | "root": "root",
93 | "version": 7
94 | }
95 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "Dura build and development environment";
3 |
4 | # Provides abstraction to boiler-code when specifying multi-platform outputs.
5 | inputs = {
6 | flake-utils.url = "github:numtide/flake-utils";
7 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
8 | rust-overlay.url = "github:oxalica/rust-overlay";
9 | };
10 |
11 | outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }:
12 | flake-utils.lib.eachDefaultSystem (system:
13 | let
14 | shortRev = if (self ? shortRev) then self.shortRev else "dev-${self.lastModifiedDate}";
15 |
16 | pkgs = import nixpkgs {
17 | inherit system;
18 | overlays = [ rust-overlay.overlay ];
19 | };
20 |
21 | dura = pkgs.rustPlatform.buildRustPackage {
22 | pname = "dura";
23 | version = "${shortRev}";
24 | description = "A background process that saves uncommited changes on git";
25 |
26 | src = self;
27 |
28 | cargoLock = {
29 | lockFile = self + "/Cargo.lock";
30 | };
31 |
32 | buildInputs = [
33 | pkgs.openssl
34 | ];
35 |
36 | nativeBuildInputs = [
37 | pkgs.rust-bin.stable.latest.minimal
38 | pkgs.pkg-config
39 | ];
40 |
41 | DURA_VERSION_SUFFIX = "${shortRev}";
42 | };
43 |
44 | packages = flake-utils.lib.flattenTree {
45 | inherit dura;
46 | };
47 |
48 | apps = {
49 | dura = flake-utils.lib.mkApp { drv = packages.dura; };
50 | };
51 | in
52 | rec {
53 | defaultPackage = packages.dura;
54 | defaultApp = apps.dura;
55 | devShell = pkgs.mkShell {
56 | DURA_VERSION_SUFFIX = dura.version;
57 | RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
58 |
59 | buildInputs = [
60 | pkgs.openssl
61 | pkgs.pkgconfig
62 | (pkgs.rust-bin.stable.latest.default.override { extensions = [ "rust-src" ]; })
63 | ];
64 | };
65 |
66 | });
67 | }
68 |
--------------------------------------------------------------------------------
/scripts/dura.fish:
--------------------------------------------------------------------------------
1 | set script (realpath (status --current-filename))
2 | if pgrep -f $script >/dev/null 2>/dev/null
3 | exit 0
4 | end
5 |
6 | function tempfile
7 | if command -v mktemp >/dev/null 2>/dev/null
8 | command mktemp
9 | else
10 | command tempfile
11 | end
12 | end
13 |
14 | set MONITOR_TEMP_FILE (tempfile)
15 | set MONITOR_PID_FILE (tempfile)
16 |
17 | function duraMonitor
18 | pkill -P (cat $MONITOR_PID_FILE) 2>/dev/null
19 | pkill (cat $MONITOR_PID_FILE) 2>/dev/null
20 |
21 | echo '
22 | set repos (cat ~/.config/dura/config.json | jq -rc \'.repos | keys | join("§")\' 2>/dev/null)
23 | set pollingSeconds (cat ~/.config/dura/config.json | jq -r ".pollingSeconds // 5")
24 |
25 | fswatch -e .git -0 -l $pollingSeconds -r (string split "§" -- $repos) | while read -l -z path
26 | cd $path 2>/dev/null || cd (dirname $path) && cd (git rev-parse --show-toplevel) && dura capture
27 | end
28 | ' >$MONITOR_TEMP_FILE
29 |
30 | fish $MONITOR_TEMP_FILE &
31 | jobs -p >$MONITOR_PID_FILE
32 | end
33 |
34 |
35 | duraMonitor
36 | fswatch -0 -l 3 ~/.config/dura/config.json | while read -l -z path
37 | duraMonitor
38 | end
39 |
--------------------------------------------------------------------------------
/scripts/pre-commit.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # This script is intended to be run before committing. It represents what's don in CI anyway,
3 | # but should reduce the frustration of getting your commits rejected. This isn't identical to
4 | # what happens in CI, it's a much faster version, it does everything in DEBUG.
5 |
6 | # Failed commands should cause the entire script to fail immediately
7 | set -e
8 |
9 | echo "################################"
10 | echo "### cargo test"
11 | echo "################################"
12 | cargo test
13 |
14 | echo "################################"
15 | echo "### cargo clippy"
16 | echo "################################"
17 | cargo clippy --all-targets --all-features -- -D warnings
18 |
19 | echo "################################"
20 | echo "### cargo fmt"
21 | echo "################################"
22 | # This doesn't fail, it just leaves files changed
23 | cargo fmt
24 | if [[ ! -z "$(git diff-index --name-only HEAD --)" ]]; then
25 | echo "Error: Git changes present, maybe from 'cargo fmt', consider committing"
26 | exit 1
27 | fi
28 |
--------------------------------------------------------------------------------
/src/config.rs:
--------------------------------------------------------------------------------
1 | use std::collections::BTreeMap;
2 | use std::fs::{create_dir_all, File};
3 | use std::io::{BufReader, Read};
4 | use std::path::{Path, PathBuf};
5 | use std::rc::Rc;
6 | use std::{env, fs};
7 |
8 | use serde::{Deserialize, Serialize};
9 |
10 | use crate::git_repo_iter::GitRepoIter;
11 |
12 | type Result = std::result::Result>;
13 |
14 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
15 | pub struct WatchConfig {
16 | pub include: Vec,
17 | pub exclude: Vec,
18 | pub max_depth: u8,
19 | }
20 |
21 | impl WatchConfig {
22 | pub fn new() -> Self {
23 | Self {
24 | include: vec![],
25 | exclude: vec![],
26 | max_depth: 255,
27 | }
28 | }
29 | }
30 |
31 | impl Default for WatchConfig {
32 | fn default() -> Self {
33 | WatchConfig::new()
34 | }
35 | }
36 |
37 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
38 | pub struct Config {
39 | // When commit_exclude_git_config is true,
40 | // never use any git configuration to sign dura's commits.
41 | // Defaults to false
42 | #[serde(default)]
43 | pub commit_exclude_git_config: bool,
44 | pub commit_author: Option,
45 | pub commit_email: Option,
46 | pub repos: BTreeMap>,
47 | }
48 |
49 | impl Config {
50 | pub fn empty() -> Self {
51 | Self {
52 | commit_exclude_git_config: false,
53 | commit_author: None,
54 | commit_email: None,
55 | repos: BTreeMap::new(),
56 | }
57 | }
58 |
59 | pub fn default_path() -> PathBuf {
60 | Self::get_dura_config_home().join("config.toml")
61 | }
62 |
63 | /// Location of all config. By default
64 | ///
65 | /// Linux : $XDG_CONFIG_HOME/dura or $HOME/.config/dura
66 | /// macOS : $HOME/Library/Application Support
67 | /// Windows : %AppData%\Roaming\dura
68 | ///
69 | /// This can be overridden by setting DURA_CONFIG_HOME environment variable.
70 | fn get_dura_config_home() -> PathBuf {
71 | // The environment variable lets us run tests independently, but I'm sure someone will come
72 | // up with another reason to use it.
73 | if let Ok(env_var) = env::var("DURA_CONFIG_HOME") {
74 | if !env_var.is_empty() {
75 | return env_var.into();
76 | }
77 | }
78 |
79 | dirs::config_dir()
80 | .expect("Could not find your config directory. The default is ~/.config/dura but it can also \
81 | be controlled by setting the DURA_CONFIG_HOME environment variable.")
82 | .join("dura")
83 | }
84 |
85 | /// Load Config from default path
86 | pub fn load() -> Self {
87 | Self::load_file(Self::default_path().as_path()).unwrap_or_else(|_| Self::empty())
88 | }
89 |
90 | pub fn load_file(path: &Path) -> Result {
91 | let mut reader = BufReader::new(File::open(path)?);
92 |
93 | let mut buffer = Vec::new();
94 | reader.read_to_end(&mut buffer)?;
95 |
96 | let res = toml::from_slice(buffer.as_slice())?;
97 | Ok(res)
98 | }
99 |
100 | /// Save config to disk in ~/.config/dura/config.toml
101 | pub fn save(&self) {
102 | self.save_to_path(Self::default_path().as_path())
103 | }
104 |
105 | pub fn create_dir(path: &Path) {
106 | if let Some(dir) = path.parent() {
107 | create_dir_all(dir)
108 | .unwrap_or_else(|_| panic!("Failed to create directory at `{}`.\
109 | Dura stores its configuration in `{}/config.toml`, \
110 | where you can instruct dura to watch patterns of Git repositories, among other things. \
111 | See https://github.com/tkellogg/dura for more information.", dir.display(), path.display()))
112 | }
113 | }
114 |
115 | /// Attempts to create parent dirs, serialize `self` as TOML and write to disk.
116 | pub fn save_to_path(&self, path: &Path) {
117 | Self::create_dir(path);
118 |
119 | let config_string = match toml::to_string(self) {
120 | Ok(v) => v,
121 | Err(e) => {
122 | println!("Unexpected error when deserializing config: {e}");
123 | return;
124 | }
125 | };
126 |
127 | match fs::write(path, config_string) {
128 | Ok(_) => (),
129 | Err(e) => println!("Unable to initialize dura config file: {e}"),
130 | }
131 | }
132 |
133 | pub fn set_watch(&mut self, path: String, cfg: WatchConfig) {
134 | let abs_path = fs::canonicalize(path).expect("The provided path is not a directory");
135 | let abs_path = abs_path
136 | .to_str()
137 | .expect("The provided path is not valid unicode");
138 |
139 | if self.repos.contains_key(abs_path) {
140 | println!("{abs_path} is already being watched")
141 | } else {
142 | self.repos.insert(abs_path.to_string(), Rc::new(cfg));
143 | println!("Started watching {abs_path}")
144 | }
145 | }
146 |
147 | pub fn set_unwatch(&mut self, path: String) {
148 | let abs_path = fs::canonicalize(path).expect("The provided path is not a directory");
149 | let abs_path = abs_path
150 | .to_str()
151 | .expect("The provided path is not valid unicode")
152 | .to_string();
153 |
154 | match self.repos.remove(&abs_path) {
155 | Some(_) => {
156 | println!("Stopped watching {abs_path}");
157 | }
158 | None => println!("{abs_path} is not being watched"),
159 | }
160 | }
161 |
162 | pub fn git_repos(&self) -> GitRepoIter {
163 | GitRepoIter::new(self)
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/database.rs:
--------------------------------------------------------------------------------
1 | use std::fs::{create_dir_all, File};
2 | use std::io::Result;
3 | use std::path::{Path, PathBuf};
4 | use std::{env, fs, io};
5 |
6 | use serde::{Deserialize, Serialize};
7 |
8 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
9 | pub struct RuntimeLock {
10 | pub pid: Option,
11 | }
12 |
13 | impl RuntimeLock {
14 | pub fn empty() -> Self {
15 | Self { pid: None }
16 | }
17 |
18 | pub fn default_path() -> PathBuf {
19 | Self::get_dura_cache_home().join("runtime.db")
20 | }
21 |
22 | /// Location of all database files. By default
23 | ///
24 | /// Linux : $XDG_CACHE_HOME/dura or $HOME/.cache/dura
25 | /// macOS : $HOME/Library/Caches
26 | /// Windows : %AppData%\Local\dura
27 | ///
28 | /// This can be overridden by setting DURA_CACHE_HOME environment variable.
29 | fn get_dura_cache_home() -> PathBuf {
30 | // The environment variable lets us run tests independently, but I'm sure someone will come
31 | // up with another reason to use it.
32 | if let Ok(env_var) = env::var("DURA_CACHE_HOME") {
33 | if !env_var.is_empty() {
34 | return env_var.into();
35 | }
36 | }
37 |
38 | dirs::cache_dir()
39 | .expect("Could not find your cache directory. The default is ~/.cache/dura but it can also \
40 | be controlled by setting the DURA_CACHE_HOME environment variable.")
41 | .join("dura")
42 | }
43 |
44 | /// Load Config from default path
45 | pub fn load() -> Self {
46 | Self::load_file(Self::default_path().as_path()).unwrap_or_else(|_| Self::empty())
47 | }
48 |
49 | pub fn load_file(path: &Path) -> Result {
50 | let reader = io::BufReader::new(File::open(path)?);
51 | let res = serde_json::from_reader(reader)?;
52 | Ok(res)
53 | }
54 |
55 | /// Save config to disk in ~/.cache/dura/runtime.db
56 | pub fn save(&self) {
57 | self.save_to_path(Self::default_path().as_path())
58 | }
59 |
60 | pub fn create_dir(path: &Path) {
61 | if let Some(dir) = path.parent() {
62 | create_dir_all(dir).unwrap_or_else(|_| {
63 | panic!(
64 | "Failed to create directory at `{}`.\
65 | Dura stores its runtime cache in `{}/runtime.db`. \
66 | See https://github.com/tkellogg/dura for more information.",
67 | dir.display(),
68 | path.display()
69 | )
70 | })
71 | }
72 | }
73 |
74 | /// Attempts to create parent dirs, serialize `self` as JSON and write to disk.
75 | pub fn save_to_path(&self, path: &Path) {
76 | Self::create_dir(path);
77 |
78 | let json = serde_json::to_string(self).unwrap();
79 | fs::write(path, json).unwrap()
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/git_repo_iter.rs:
--------------------------------------------------------------------------------
1 | use std::collections::btree_map;
2 | use std::fs;
3 | use std::path::{Path, PathBuf};
4 | use std::rc::Rc;
5 |
6 | use crate::config::{Config, WatchConfig};
7 | use crate::snapshots;
8 |
9 | /// Internal structure to facilitate "recursion" without blowing up the stack. Without this, we
10 | /// could call self.next() recursively whenever there was an I/O error or when we reached the end
11 | /// of a directory listing. There's no stack space used because we just mutate GitRepoIter, so
12 | /// might as well turn it into a loop.
13 | enum CallState {
14 | Yield(PathBuf),
15 | Recurse,
16 | Done,
17 | }
18 |
19 | /// Iterator over all Git repos covered by a config.
20 | ///
21 | /// The process is naturally recursive, traversing a directory structure, which made it a poor fit
22 | /// for a more typical filter/map chain.
23 | ///
24 | /// Function recursion is used in a few cases:
25 | /// 1. Errors: If we get an I/O error, we'll call self.next() again
26 | /// 2. Empty iterator: If we get to the end of a sub-iterator, pop & start from the top
27 | ///
28 | pub struct GitRepoIter<'a> {
29 | config_iter: btree_map::Iter<'a, String, Rc>,
30 | /// A stack, because we can't use recursion with an iterator (at least not between elements)
31 | sub_iter: Vec<(Rc, Rc, fs::ReadDir)>,
32 | }
33 |
34 | impl<'a> GitRepoIter<'a> {
35 | pub fn new(config: &'a Config) -> Self {
36 | Self {
37 | config_iter: config.repos.iter(),
38 | sub_iter: Vec::new(),
39 | }
40 | }
41 |
42 | fn get_next(&mut self) -> CallState {
43 | // pop
44 | //
45 | // Use pop here to manage the lifetime of the iterator. If we used last/peek, we would
46 | // borrow a shared reference, which precludes us from borrowing as mutable when we want to
47 | // use the iterator. But that means we have to return it to the vec.
48 | match self.sub_iter.pop() {
49 | Some((base_path, watch_config, mut dir_iter)) => {
50 | let mut next_next: Option<(Rc, Rc, fs::ReadDir)> = None;
51 | let mut ret_val = CallState::Recurse;
52 | let max_depth: usize = watch_config.max_depth.into();
53 | if let Some(Ok(entry)) = dir_iter.next() {
54 | let child_path = entry.path();
55 | if is_valid_directory(base_path.as_path(), child_path.as_path(), &watch_config)
56 | {
57 | if snapshots::is_repo(child_path.as_path()) {
58 | ret_val = CallState::Yield(child_path);
59 | } else if self.sub_iter.len() < max_depth {
60 | if let Ok(child_dir_iter) = fs::read_dir(child_path.as_path()) {
61 | next_next = Some((
62 | Rc::clone(&base_path),
63 | Rc::clone(&watch_config),
64 | child_dir_iter,
65 | ))
66 | }
67 | }
68 | }
69 | // un-pop
70 | self.sub_iter
71 | .push((Rc::clone(&base_path), Rc::clone(&watch_config), dir_iter));
72 | }
73 | if let Some(tuple) = next_next {
74 | // directory recursion
75 | self.sub_iter.push(tuple);
76 | }
77 | ret_val
78 | }
79 | None => {
80 | // Finished dir, queue up next hashmap pair
81 | match self.config_iter.next() {
82 | Some((base_path, watch_config)) => {
83 | let path = PathBuf::from(base_path);
84 | let dir_iter_opt = path.parent().and_then(|p| fs::read_dir(p).ok());
85 | if let Some(dir_iter) = dir_iter_opt {
86 | // clone because we're going from more global to less global scope
87 | self.sub_iter
88 | .push((Rc::new(path), Rc::clone(watch_config), dir_iter));
89 | }
90 | CallState::Recurse
91 | }
92 | // The end. The real end. This is it.
93 | None => CallState::Done,
94 | }
95 | }
96 | }
97 | }
98 | }
99 |
100 | impl<'a> Iterator for GitRepoIter<'a> {
101 | type Item = PathBuf;
102 |
103 | fn next(&mut self) -> Option {
104 | loop {
105 | match self.get_next() {
106 | CallState::Yield(path) => return Some(path),
107 | CallState::Recurse => continue,
108 | CallState::Done => return None,
109 | }
110 | }
111 | }
112 | }
113 |
114 | /// Checks the provided `child_path` is a directory.
115 | /// If either `includes` or `excludes` are set,
116 | /// checks whether the path is included/excluded respectively.
117 | fn is_valid_directory(base_path: &Path, child_path: &Path, value: &WatchConfig) -> bool {
118 | if !child_path.is_dir() {
119 | return false;
120 | }
121 |
122 | if !child_path.starts_with(base_path) {
123 | return false;
124 | }
125 |
126 | let includes = &value.include;
127 | let excludes = &value.exclude;
128 |
129 | let mut include = true;
130 |
131 | if !excludes.is_empty() {
132 | include = !excludes
133 | .iter()
134 | .any(|exclude| child_path.starts_with(base_path.join(exclude)));
135 | }
136 |
137 | if !include && !includes.is_empty() {
138 | include = includes
139 | .iter()
140 | .any(|include| base_path.join(include).starts_with(child_path));
141 | }
142 |
143 | include
144 | }
145 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod config;
2 | pub mod database;
3 | pub mod git_repo_iter;
4 | pub mod log;
5 | pub mod logger;
6 | pub mod metrics;
7 | pub mod poll_guard;
8 | pub mod poller;
9 | pub mod snapshots;
10 |
--------------------------------------------------------------------------------
/src/log.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Debug;
2 | use std::time::{Duration, Instant};
3 |
4 | use hdrhistogram::Histogram;
5 | use serde::{Deserialize, Serialize};
6 | use tracing::trace;
7 |
8 | use crate::snapshots::CaptureStatus;
9 |
10 | #[derive(Debug, Serialize, Deserialize)]
11 | pub enum Operation {
12 | Snapshot {
13 | repo: String,
14 | op: Option,
15 | error: Option,
16 | latency: f32,
17 | },
18 | CollectStats {
19 | per_dir_stats: Histo,
20 | loop_stats: Histo,
21 | },
22 | }
23 |
24 | impl Operation {
25 | pub fn should_log(&self) -> bool {
26 | match self {
27 | Operation::Snapshot {
28 | repo: _,
29 | op,
30 | error,
31 | latency: _,
32 | } => op.is_some() || error.is_some(),
33 | Operation::CollectStats { .. } => {
34 | true // logic punted to StatCollector
35 | }
36 | }
37 | }
38 |
39 | pub fn log_str(&mut self) -> String {
40 | // This unwrap seems safe, afaict. We're not cramming any user supplied strings in here.
41 | serde_json::to_string(self).expect("Couldn't serialize to JSON")
42 | }
43 | }
44 |
45 | #[derive(Debug, Serialize, Deserialize)]
46 | struct Stats {
47 | dir_stats: Histo,
48 | loop_stats: Histo,
49 | }
50 |
51 | /// A serializable form of a hdrhistogram, mainly just for logging out
52 | /// in a way we want to read it
53 | #[derive(Debug, Serialize, Deserialize)]
54 | pub struct Histo {
55 | mean: f64,
56 | count: u64,
57 | min: u64,
58 | max: u64,
59 | percentiles: Vec,
60 | }
61 |
62 | /// For serializing to JSON
63 | ///
64 | /// Choice of tiny names because this one shows up a lot, one
65 | /// for each percentile bucket. It shows a lot more data
66 | /// points at the upper percentiles, so we need to capture
67 | /// both percentile and associated millisecond value.
68 | #[derive(Debug, Serialize, Deserialize)]
69 | pub struct Percentile {
70 | pct: f64,
71 | val: u64,
72 | }
73 |
74 | impl Histo {
75 | pub fn from_histogram(hist: &Histogram) -> Histo {
76 | Self {
77 | mean: hist.mean(),
78 | count: hist.len(),
79 | min: hist.min(),
80 | max: hist.max(),
81 | percentiles: hist
82 | .iter_quantiles(2)
83 | .map(|q| Percentile {
84 | pct: q.percentile(),
85 | val: q.value_iterated_to(),
86 | })
87 | .collect(),
88 | }
89 | }
90 | }
91 |
92 | #[derive(Debug)]
93 | pub struct StatCollector {
94 | start: Instant,
95 | per_dir_stats: Histogram,
96 | loop_stats: Histogram,
97 | }
98 |
99 | /// 5 minutes in milliseconds
100 | const MAX_LATENCY_IMAGINABLE: u64 = 5 * 60 * 1000;
101 |
102 | /// How many seconds between logging stats?
103 | const STAT_LOG_INTERVAL: f32 = 600.0;
104 |
105 | impl StatCollector {
106 | pub fn new() -> Self {
107 | Self {
108 | start: Instant::now(),
109 | per_dir_stats: Histogram::::new_with_max(MAX_LATENCY_IMAGINABLE, 3).unwrap(),
110 | loop_stats: Histogram::::new_with_max(MAX_LATENCY_IMAGINABLE, 3).unwrap(),
111 | }
112 | }
113 |
114 | pub fn to_op(&self) -> Operation {
115 | Operation::CollectStats {
116 | per_dir_stats: Histo::from_histogram(&self.per_dir_stats),
117 | loop_stats: Histo::from_histogram(&self.loop_stats),
118 | }
119 | }
120 |
121 | pub fn should_log(&self) -> bool {
122 | let elapsed = (Instant::now() - self.start).as_secs_f32();
123 | trace!(
124 | elapsed = elapsed,
125 | target = STAT_LOG_INTERVAL,
126 | "Should we log metrics?"
127 | );
128 | elapsed > STAT_LOG_INTERVAL
129 | }
130 |
131 | pub fn log_str(&mut self) -> String {
132 | let mut op = self.to_op();
133 | let ret = op.log_str();
134 | self.reset();
135 | ret
136 | }
137 |
138 | fn reset(&mut self) {
139 | self.start = Instant::now();
140 | self.per_dir_stats.clear();
141 | self.loop_stats.clear();
142 | }
143 |
144 | /// Record the time it takes to process a single directory. Mainly interested to see if
145 | /// there's any outliers, the histogram should be interesting.
146 | pub fn record_dir(&mut self, latency: Duration) {
147 | let value = latency.as_millis().try_into().unwrap();
148 | self.per_dir_stats.saturating_record(value);
149 | }
150 |
151 | /// Record the time it takes to go through all directories. I expect mean will be the
152 | /// most interesting datum. Mainly for projecting CPU usage.
153 | pub fn record_loop(&mut self, latency: Duration) {
154 | let value = latency.as_millis().try_into().unwrap();
155 | self.loop_stats.saturating_record(value);
156 | }
157 | }
158 |
159 | impl Default for StatCollector {
160 | fn default() -> Self {
161 | Self::new()
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/logger.rs:
--------------------------------------------------------------------------------
1 | use chrono::Utc;
2 | use serde::ser::SerializeMap;
3 | use serde::Serializer;
4 | use std::collections::BTreeMap;
5 | use std::fmt;
6 | use std::io::Write;
7 | use tracing::field::{Field, Visit};
8 | use tracing::Subscriber;
9 | use tracing_subscriber::fmt::MakeWriter;
10 | use tracing_subscriber::Layer;
11 |
12 | pub struct NestedJsonLayer MakeWriter<'a> + 'static> {
13 | mw: W,
14 | }
15 |
16 | impl MakeWriter<'a> + 'static> NestedJsonLayer {
17 | pub fn new(mw: W) -> Self {
18 | Self { mw }
19 | }
20 |
21 | pub fn serialize_and_write(
22 | &self,
23 | event: &tracing::Event<'_>,
24 | hm: BTreeMap<&'static str, serde_json::Value>,
25 | ) -> Result, serde_json::Error> {
26 | let mut buffer = Vec::new();
27 | let mut serializer = serde_json::Serializer::new(&mut buffer);
28 | let mut ser_map = serializer.serialize_map(None)?;
29 |
30 | ser_map.serialize_entry("target", event.metadata().target())?;
31 | ser_map.serialize_entry("file", &event.metadata().file())?;
32 | ser_map.serialize_entry("name", event.metadata().name())?;
33 | ser_map.serialize_entry("level", &format!("{:?}", event.metadata().level()))?;
34 | ser_map.serialize_entry("fields", &hm)?;
35 | ser_map.serialize_entry("time", &Utc::now().to_rfc3339())?;
36 | ser_map.end()?;
37 | Ok(buffer)
38 | }
39 |
40 | pub fn write_all(&self, mut buffer: Vec) -> std::io::Result<()> {
41 | buffer.write_all(b"\n")?;
42 | self.mw.make_writer().write_all(&buffer)
43 | }
44 | }
45 |
46 | impl Layer for NestedJsonLayer
47 | where
48 | S: Subscriber,
49 | W: for<'a> MakeWriter<'a> + 'static,
50 | {
51 | fn on_event(
52 | &self,
53 | event: &tracing::Event<'_>,
54 | _ctx: tracing_subscriber::layer::Context<'_, S>,
55 | ) {
56 | let mut visitor = JsonVisitor::default();
57 | event.record(&mut visitor);
58 |
59 | if let Ok(buffer) = self.serialize_and_write(event, visitor.0) {
60 | {
61 | let _ = self.write_all(buffer);
62 | }
63 | }
64 | }
65 | }
66 |
67 | #[derive(Default)]
68 | struct JsonVisitor(BTreeMap<&'static str, serde_json::Value>);
69 |
70 | impl Visit for JsonVisitor {
71 | fn record_i64(&mut self, field: &Field, value: i64) {
72 | self.0.insert(field.name(), value.into());
73 | }
74 |
75 | fn record_u64(&mut self, field: &Field, value: u64) {
76 | self.0.insert(field.name(), value.into());
77 | }
78 |
79 | fn record_bool(&mut self, field: &Field, value: bool) {
80 | self.0.insert(field.name(), value.into());
81 | }
82 |
83 | fn record_str(&mut self, field: &Field, value: &str) {
84 | match serde_json::from_str::(value) {
85 | Ok(value) => {
86 | self.0.insert(field.name(), value);
87 | }
88 | Err(_) => {
89 | self.0.insert(field.name(), value.to_string().into());
90 | }
91 | }
92 | }
93 |
94 | fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
95 | self.0.insert(field.name(), value.to_string().into());
96 | }
97 |
98 | fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
99 | let s = format!("{value:?}");
100 | match serde_json::from_str::(&s) {
101 | Ok(value) => {
102 | self.0.insert(field.name(), value);
103 | }
104 | Err(_) => {
105 | self.0.insert(field.name(), s.into());
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::fs::{File, OpenOptions};
2 | use std::io::{stdin, stdout, BufReader, BufWriter, Read, Write};
3 | use std::path::Path;
4 | use std::process;
5 |
6 | use clap::builder::IntoResettable;
7 | use clap::{
8 | arg, crate_authors, crate_description, crate_name, crate_version, value_parser, Arg, Command,
9 | };
10 | use dura::config::{Config, WatchConfig};
11 | use dura::database::RuntimeLock;
12 | use dura::logger::NestedJsonLayer;
13 | use dura::metrics;
14 | use dura::poller;
15 | use dura::snapshots;
16 | use tracing::info;
17 | use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt;
18 | use tracing_subscriber::util::SubscriberInitExt;
19 | use tracing_subscriber::{EnvFilter, Registry};
20 |
21 | #[tokio::main]
22 | async fn main() {
23 | if !check_if_user() {
24 | eprintln!("Dura cannot be run as root, to avoid data corruption");
25 | process::exit(1);
26 | }
27 |
28 | let cwd = std::env::current_dir().expect("Failed to get current directory");
29 |
30 | let suffix = option_env!("DURA_VERSION_SUFFIX")
31 | .map(|v| format!(" @ {}", v))
32 | .unwrap_or_else(|| String::from(""));
33 |
34 | let version = format!("{}{}", crate_version!(), suffix);
35 |
36 | let arg_directory = Arg::new("directory")
37 | .default_value(cwd.into_os_string().into_resettable())
38 | .help("The directory to watch. Defaults to current directory");
39 |
40 | let matches = Command::new(crate_name!())
41 | .about(crate_description!())
42 | .version(version.into_resettable())
43 | .subcommand_required(true)
44 | .arg_required_else_help(true)
45 | .author(crate_authors!())
46 | .subcommand(
47 | Command::new("capture")
48 | .short_flag('C')
49 | .long_flag("capture")
50 | .about("Run a single backup of an entire repository. This is the one single iteration of the `serve` control loop.")
51 | .arg(arg_directory.clone())
52 | )
53 | .subcommand(
54 | Command::new("serve")
55 | .short_flag('S')
56 | .long_flag("serve")
57 | .about("Starts the worker that listens for file changes. If another process is already running, this will do it's best to terminate the other process.")
58 | .arg(
59 | arg!(--logfile )
60 | .required(false)
61 | .help("Sets custom logfile. Default is logging to stdout")
62 | ))
63 | .subcommand(
64 | Command::new("watch")
65 | .short_flag('W')
66 | .long_flag("watch")
67 | .about("Add the current working directory as a repository to watch.")
68 | .arg(arg_directory.clone())
69 | .arg(arg!(-i --include)
70 | .required(false)
71 | .action(clap::builder::ArgAction::Set)
72 | .num_args(0..)
73 | .value_parser(value_parser!(String))
74 | .value_delimiter(',')
75 | .help("Overrides excludes by re-including specific directories relative to the watch directory.")
76 | )
77 | .arg(arg!(-e --exclude)
78 | .required(false)
79 | .action(clap::builder::ArgAction::Set)
80 | .num_args(0..)
81 | .value_parser(value_parser!(String))
82 | .value_delimiter(',')
83 | .help("Excludes specific directories relative to the watch directory")
84 | )
85 | .arg(arg!(-d --maxdepth)
86 | .required(false)
87 | .action(clap::builder::ArgAction::Set)
88 | .value_parser(value_parser!(String))
89 | .default_value(&"255".to_string())
90 | .num_args(0..=1)
91 | .help("Determines the depth to recurse into when scanning directories")
92 | )
93 | )
94 | .subcommand(
95 | Command::new("unwatch")
96 | .short_flag('U')
97 | .long_flag("unwatch")
98 | .about("Remove the current working directory as a repository to watch.")
99 | .arg(arg_directory)
100 | )
101 | .subcommand(
102 | Command::new("kill")
103 | .short_flag('K')
104 | .long_flag("kill")
105 | .about("Stop the running worker (should only be a single worker).")
106 | )
107 | .subcommand(
108 | Command::new("metrics")
109 | .short_flag('M')
110 | .long_flag("metrics")
111 | .about("Convert logs into richer metrics about snapshots.")
112 | .arg(arg!(-i --input)
113 | .required(false)
114 | .num_args(1)
115 | .help("The log file to read. Defaults to stdin.")
116 | )
117 | .arg(arg!(-o --output)
118 | .required(false)
119 | .num_args(1)
120 | .help("The json file to write. Defaults to stdout.")
121 | )
122 | )
123 | .get_matches();
124 |
125 | match matches.subcommand() {
126 | Some(("capture", arg_matches)) => {
127 | let dir = Path::new(arg_matches.get_one::("directory").unwrap());
128 | match snapshots::capture(dir) {
129 | Ok(oid_opt) => {
130 | if let Some(oid) = oid_opt {
131 | println!("{oid}");
132 | }
133 | }
134 | Err(e) => {
135 | println!("Dura capture failed: {e}");
136 | process::exit(1);
137 | }
138 | }
139 | }
140 | Some(("serve", arg_matches)) => {
141 | let env_filter =
142 | EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
143 |
144 | match arg_matches.get_one::("logfile") {
145 | Some(logfile) => {
146 | let file = logfile.to_string();
147 | Registry::default()
148 | .with(env_filter)
149 | .with(NestedJsonLayer::new(move || {
150 | let result_open_file =
151 | OpenOptions::new().append(true).create(true).open(&file);
152 | match result_open_file {
153 | Ok(f) => f,
154 | Err(e) => {
155 | eprintln!("Unable to open file {file} for logging due to {e}");
156 | std::process::exit(1);
157 | }
158 | }
159 | }))
160 | .init();
161 | }
162 | None => {
163 | Registry::default()
164 | .with(env_filter)
165 | .with(NestedJsonLayer::new(std::io::stdout))
166 | .init();
167 | }
168 | }
169 |
170 | info!("Started serving with dura v{}", crate_version!());
171 | poller::start().await;
172 | }
173 | Some(("watch", arg_matches)) => {
174 | let dir = Path::new(arg_matches.get_one::("directory").unwrap());
175 |
176 | let include = arg_matches
177 | .get_many::("include")
178 | .unwrap_or_default()
179 | .map(|s| s.to_string())
180 | .collect::>();
181 | let exclude = arg_matches
182 | .get_many::("exclude")
183 | .unwrap_or_default()
184 | .map(|s| s.to_string())
185 | .collect::>();
186 | let max_depth = arg_matches
187 | .get_one::("maxdepth")
188 | .unwrap_or(&"255".to_string())
189 | .parse::()
190 | .expect("Max depth must be between 0-255");
191 |
192 | let watch_config = WatchConfig {
193 | include,
194 | exclude,
195 | max_depth,
196 | };
197 |
198 | watch_dir(dir, watch_config);
199 | }
200 | Some(("unwatch", arg_matches)) => {
201 | let dir = Path::new(arg_matches.get_one::("directory").unwrap());
202 | unwatch_dir(dir)
203 | }
204 | Some(("kill", _)) => {
205 | kill();
206 | }
207 | Some(("metrics", arg_matches)) => {
208 | let mut input: Box = match arg_matches.get_one::("input") {
209 | Some(input) => Box::new(
210 | File::open(input).unwrap_or_else(|_| panic!("Couldn't open '{}'", input)),
211 | ),
212 | None => Box::new(BufReader::new(stdin())),
213 | };
214 | let mut output: Box = match arg_matches.get_one::("output") {
215 | Some(output) => Box::new(
216 | File::open(output).unwrap_or_else(|_| panic!("Couldn't open '{}'", output)),
217 | ),
218 | None => Box::new(BufWriter::new(stdout())),
219 | };
220 | if let Err(e) = metrics::get_snapshot_metrics(&mut input, &mut output) {
221 | eprintln!("Failed: {}", e);
222 | process::exit(1);
223 | }
224 | }
225 | _ => unreachable!(),
226 | }
227 | }
228 |
229 | fn watch_dir(path: &std::path::Path, watch_config: WatchConfig) {
230 | let mut config = Config::load();
231 | let path = path
232 | .to_str()
233 | .expect("The provided path is not valid unicode")
234 | .to_string();
235 |
236 | config.set_watch(path, watch_config);
237 | config.save();
238 | }
239 |
240 | fn unwatch_dir(path: &std::path::Path) {
241 | let mut config = Config::load();
242 | let path = path
243 | .to_str()
244 | .expect("The provided path is not valid unicode")
245 | .to_string();
246 |
247 | config.set_unwatch(path);
248 | config.save();
249 | }
250 |
251 | #[cfg(all(unix))]
252 | fn check_if_user() -> bool {
253 | sudo::check() != sudo::RunningAs::Root
254 | }
255 |
256 | #[cfg(target_os = "windows")]
257 | fn check_if_user() -> bool {
258 | true
259 | }
260 |
261 | /// kills running dura poller
262 | ///
263 | /// poller's check to make sure that their pid is the same as the pid
264 | /// found in config, and if they are not the same they exit. This
265 | /// function does not actually kill a poller but instead indicates
266 | /// that any living poller should exit during their next check.
267 | fn kill() {
268 | let mut runtime_lock = RuntimeLock::load();
269 | runtime_lock.pid = None;
270 | runtime_lock.save();
271 | }
272 |
--------------------------------------------------------------------------------
/src/metrics.rs:
--------------------------------------------------------------------------------
1 | use crate::log::Operation;
2 | use git2::{Oid, Repository};
3 | use serde_json::map::Map;
4 | use serde_json::value::from_value;
5 | use serde_json::{json, Number, Value};
6 | use std::collections::HashMap;
7 | use std::io::{self, BufRead, Write};
8 | use std::rc::Rc;
9 |
10 | type FlexResult = std::result::Result>;
11 |
12 | /// Reads an input stream that contains dura logs and enriches them with more analytics-ready info
13 | /// like number of insertions & deletions. The result is written back out to an output stream.
14 | pub fn get_snapshot_metrics(
15 | input: &mut dyn io::Read,
16 | output: &mut dyn io::Write,
17 | ) -> FlexResult<()> {
18 | let mut reader = io::BufReader::new(input);
19 | let mut writer = io::BufWriter::new(output);
20 | let mut line: u64 = 0; // for printing better error messages
21 | let mut repo_cache: HashMap> = HashMap::new();
22 | loop {
23 | line += 1;
24 | let mut input_line = String::new();
25 | if reader.read_line(&mut input_line)? == 0 {
26 | return Ok(());
27 | }
28 | match scrape_log(input_line) {
29 | Ok(Some(mut output)) => {
30 | scrape_git(&mut output, &mut repo_cache)?;
31 | writeln!(&mut writer, "{output}")?;
32 | }
33 | Ok(None) => {}
34 | // Seems like a good way to report errors, idk...
35 | Err(e) => eprintln!("line {line}: {e}"),
36 | }
37 | }
38 | }
39 |
40 | /// Scrape information out of the snapshot log.
41 | fn scrape_log(line: String) -> serde_json::Result