Compare commits

..
206 Commits
Author SHA1 Message Date
tamakyi d5a1612329 git-sync-web 自动提交 2026-09-09 20:14:20 2026-09-09 20:14:20 +08:00
tamakyi 2862f662c4 git-sync-web 自动提交 2026-09-09 20:02:57 2026-09-09 20:02:58 +08:00
tamakyi 0ac410d954 git-sync-web 自动提交 2026-09-09 19:40:15 2026-09-09 19:40:15 +08:00
tamakyi 904ddcc0ba git-sync-web 自动提交 2026-09-09 19:28:51 2026-09-09 19:28:51 +08:00
tamakyi 5f555b447d summary 2026-09-09 19:08:59 +08:00
tamakyi 4e6a743179 git-sync-web 自动提交 2026-09-09 18:09:14 2026-09-09 18:09:15 +08:00
tamakyi de2295bb5f git-sync-web 自动提交 2026-09-09 18:02:17 2026-09-09 18:02:18 +08:00
tamakyi b260720183 fix config commit 2026-09-09 17:18:58 +08:00
tamakyi d708a644d2 git-sync-web 自动提交 2026-09-09 17:10:25 2026-09-09 17:10:25 +08:00
tamakyi 80bd1136f2 git-sync-web 自动提交 2026-09-09 16:33:32 2026-09-09 16:33:32 +08:00
tamakyi 5cdffecb44 git-sync-web 自动提交 2026-09-09 15:39:21 2026-09-09 15:39:22 +08:00
tamakyi 4232da5ced git-sync-web 自动提交 2026-09-09 15:27:47 2026-09-09 15:27:48 +08:00
tamakyi eae64f5c47 git-sync-web 自动提交 2026-09-09 15:18:11 2026-09-09 15:18:11 +08:00
tamakyi 86c11864f0 git-sync-web 自动提交 2026-09-09 15:06:45 2026-09-09 15:06:45 +08:00
tamakyi a1ccd4fe6a git-sync-web 自动提交 2026-09-09 14:55:18 2026-09-09 14:55:18 +08:00
tamakyi d48bf772c7 git-sync-web 自动提交 2026-09-09 14:43:50 2026-09-09 14:43:51 +08:00
tamakyi ab249cbf7f git-sync-web 自动提交 2026-09-09 14:09:32 2026-09-09 14:09:33 +08:00
tamakyi f161424dcd git-sync-web 自动提交 2026-09-09 13:58:07 2026-09-09 13:58:08 +08:00
tamakyi bba6a3362e git-sync-web 自动提交 2026-09-09 13:46:40 2026-09-09 13:46:41 +08:00
tamakyi b921569b72 git-sync-web 自动提交 2026-09-09 12:49:43 2026-09-09 12:49:44 +08:00
tamakyi d4f10ab574 git-sync-web 自动提交 2026-09-09 12:38:18 2026-09-09 12:38:18 +08:00
tamakyi 0602b89276 git-sync-web 自动提交 2026-09-09 12:26:53 2026-09-09 12:26:53 +08:00
tamakyi a832d0659a git-sync-web 自动提交 2026-09-09 12:04:01 2026-09-09 12:04:02 +08:00
tamakyi 05d6f1bc84 git-sync-web 自动提交 2026-09-09 11:53:34 2026-09-09 11:53:35 +08:00
tamakyi 2dd4a46ad6 feat(ui): always-visible live activity panel - real-time sync log for all repos 2026-09-09 10:57:46 +08:00
tamakyi 7138f65272 chore: ignore runtime logs/ & stats/ so auto-commit stops churning the repo 2026-09-09 10:57:43 +08:00
tamakyi 7db13aa225 git-sync-web 自动提交 2026-09-09 09:13:46 2026-09-09 09:13:46 +08:00
tamakyi 6c2c1ef77e git-sync-web 自动提交 2026-09-09 09:07:32 2026-09-09 09:07:32 +08:00
tamakyi 9ea53d2bbf git-sync-web 自动提交 2026-09-09 09:01:19 2026-09-09 09:01:19 +08:00
tamakyi fd7ac557fc git-sync-web 自动提交 2026-09-09 08:55:05 2026-09-09 08:55:06 +08:00
tamakyi d8bea94436 git-sync-web 自动提交 2026-09-09 08:48:51 2026-09-09 08:48:51 +08:00
tamakyi 40391879d1 git-sync-web 自动提交 2026-09-09 08:42:27 2026-09-09 08:42:27 +08:00
tamakyi 9f8b4cde55 git-sync-web 自动提交 2026-09-09 08:36:13 2026-09-09 08:36:13 +08:00
tamakyi 402c5ef32f git-sync-web 自动提交 2026-09-09 08:29:59 2026-09-09 08:29:59 +08:00
tamakyi 0d0b8e6045 git-sync-web 自动提交 2026-09-09 08:23:45 2026-09-09 08:23:46 +08:00
tamakyi fbe02e182b git-sync-web 自动提交 2026-09-09 08:17:32 2026-09-09 08:17:32 +08:00
tamakyi e95f1a64bf git-sync-web 自动提交 2026-09-09 08:11:17 2026-09-09 08:11:17 +08:00
tamakyi f8480e93f8 git-sync-web 自动提交 2026-09-09 08:05:02 2026-09-09 08:05:03 +08:00
tamakyi e884770628 git-sync-web 自动提交 2026-09-09 07:58:48 2026-09-09 07:58:48 +08:00
tamakyi 2d82bf0792 git-sync-web 自动提交 2026-09-09 07:52:34 2026-09-09 07:52:34 +08:00
tamakyi d7d81816fe git-sync-web 自动提交 2026-09-09 07:46:21 2026-09-09 07:46:21 +08:00
tamakyi ca0e5dad8e git-sync-web 自动提交 2026-09-09 07:40:07 2026-09-09 07:40:08 +08:00
tamakyi 7c33be0e4f git-sync-web 自动提交 2026-09-09 07:33:53 2026-09-09 07:33:53 +08:00
tamakyi 9fb9fdb7ab git-sync-web 自动提交 2026-09-09 07:27:39 2026-09-09 07:27:40 +08:00
tamakyi f8ab1deaaf git-sync-web 自动提交 2026-09-09 07:21:26 2026-09-09 07:21:26 +08:00
tamakyi c0a5fcb5dc git-sync-web 自动提交 2026-09-09 07:15:12 2026-09-09 07:15:12 +08:00
tamakyi 4944059eff git-sync-web 自动提交 2026-09-09 07:08:59 2026-09-09 07:08:59 +08:00
tamakyi 088e089876 git-sync-web 自动提交 2026-09-09 07:02:43 2026-09-09 07:02:43 +08:00
tamakyi d43c18c05d git-sync-web 自动提交 2026-09-09 06:56:29 2026-09-09 06:56:29 +08:00
tamakyi 0fbf387048 git-sync-web 自动提交 2026-09-09 06:50:13 2026-09-09 06:50:13 +08:00
tamakyi ab8e1d68ec git-sync-web 自动提交 2026-09-09 06:43:59 2026-09-09 06:43:59 +08:00
tamakyi c9e8d33c8c git-sync-web 自动提交 2026-09-09 06:37:45 2026-09-09 06:37:45 +08:00
tamakyi ae2ee3cfd3 git-sync-web 自动提交 2026-09-09 06:31:32 2026-09-09 06:31:32 +08:00
tamakyi 4789cd8acf git-sync-web 自动提交 2026-09-09 06:25:19 2026-09-09 06:25:19 +08:00
tamakyi a28700b578 git-sync-web 自动提交 2026-09-09 06:19:05 2026-09-09 06:19:05 +08:00
tamakyi 31ee72b0b0 git-sync-web 自动提交 2026-09-09 06:12:49 2026-09-09 06:12:49 +08:00
tamakyi ce21fcdde4 git-sync-web 自动提交 2026-09-09 06:06:36 2026-09-09 06:06:36 +08:00
tamakyi dbc2d787b3 git-sync-web 自动提交 2026-09-09 06:00:23 2026-09-09 06:00:23 +08:00
tamakyi 9383de0790 git-sync-web 自动提交 2026-09-09 05:54:08 2026-09-09 05:54:08 +08:00
tamakyi 9989fc89ea git-sync-web 自动提交 2026-09-09 05:47:53 2026-09-09 05:47:53 +08:00
tamakyi 7daf60b3a9 git-sync-web 自动提交 2026-09-09 05:41:39 2026-09-09 05:41:40 +08:00
tamakyi 4569ffd446 git-sync-web 自动提交 2026-09-09 05:35:26 2026-09-09 05:35:26 +08:00
tamakyi 48c6210f32 git-sync-web 自动提交 2026-09-09 05:29:12 2026-09-09 05:29:12 +08:00
tamakyi 51cd176613 git-sync-web 自动提交 2026-09-09 05:22:57 2026-09-09 05:22:57 +08:00
tamakyi 3d92effede git-sync-web 自动提交 2026-09-09 05:16:43 2026-09-09 05:16:44 +08:00
tamakyi 9e199f2791 git-sync-web 自动提交 2026-09-09 05:10:30 2026-09-09 05:10:30 +08:00
tamakyi 877cc3ee67 git-sync-web 自动提交 2026-09-09 05:04:16 2026-09-09 05:04:17 +08:00
tamakyi 8cf4252bc6 git-sync-web 自动提交 2026-09-09 04:58:03 2026-09-09 04:58:03 +08:00
tamakyi 8a93f9a2bf git-sync-web 自动提交 2026-09-09 04:51:49 2026-09-09 04:51:49 +08:00
tamakyi 15d0ce13d6 git-sync-web 自动提交 2026-09-09 04:45:35 2026-09-09 04:45:35 +08:00
tamakyi 2a59c1b627 git-sync-web 自动提交 2026-09-09 04:39:20 2026-09-09 04:39:20 +08:00
tamakyi 3951b589a4 git-sync-web 自动提交 2026-09-09 04:33:06 2026-09-09 04:33:07 +08:00
tamakyi 9523de99fa git-sync-web 自动提交 2026-09-09 04:26:52 2026-09-09 04:26:52 +08:00
tamakyi 19bfe4c8e6 git-sync-web 自动提交 2026-09-09 04:20:36 2026-09-09 04:20:36 +08:00
tamakyi 9dccc835d8 git-sync-web 自动提交 2026-09-09 04:14:19 2026-09-09 04:14:20 +08:00
tamakyi ebcb639a84 git-sync-web 自动提交 2026-09-09 04:08:05 2026-09-09 04:08:06 +08:00
tamakyi 67e7ec0289 git-sync-web 自动提交 2026-09-09 04:01:50 2026-09-09 04:01:50 +08:00
tamakyi 69f558dd41 git-sync-web 自动提交 2026-09-09 03:55:37 2026-09-09 03:55:37 +08:00
tamakyi 4c6ba9a096 git-sync-web 自动提交 2026-09-09 03:49:20 2026-09-09 03:49:20 +08:00
tamakyi 130047b8e0 git-sync-web 自动提交 2026-09-09 03:43:06 2026-09-09 03:43:06 +08:00
tamakyi b9018b1bf0 git-sync-web 自动提交 2026-09-09 03:36:53 2026-09-09 03:36:54 +08:00
tamakyi 3b839d19ad git-sync-web 自动提交 2026-09-09 03:30:38 2026-09-09 03:30:39 +08:00
tamakyi 5b8f32d766 git-sync-web 自动提交 2026-09-09 03:24:26 2026-09-09 03:24:26 +08:00
tamakyi 58cf9a3ba8 git-sync-web 自动提交 2026-09-09 03:18:10 2026-09-09 03:18:10 +08:00
tamakyi 9074b36a6a git-sync-web 自动提交 2026-09-09 03:11:56 2026-09-09 03:11:56 +08:00
tamakyi b4d94a683a git-sync-web 自动提交 2026-09-09 03:05:42 2026-09-09 03:05:43 +08:00
tamakyi 0bf7cde8a1 git-sync-web 自动提交 2026-09-09 02:59:28 2026-09-09 02:59:28 +08:00
tamakyi 9ff1639180 git-sync-web 自动提交 2026-09-09 02:53:12 2026-09-09 02:53:12 +08:00
tamakyi 6326ab6cac git-sync-web 自动提交 2026-09-09 02:40:53 2026-09-09 02:40:53 +08:00
tamakyi 37530fd9f4 git-sync-web 自动提交 2026-09-09 02:34:37 2026-09-09 02:34:37 +08:00
tamakyi dc245eadfd git-sync-web 自动提交 2026-09-09 02:28:22 2026-09-09 02:28:22 +08:00
tamakyi 7f8b7c7df7 git-sync-web 自动提交 2026-09-09 02:22:04 2026-09-09 02:22:05 +08:00
tamakyi 200e275f69 git-sync-web 自动提交 2026-09-09 02:15:51 2026-09-09 02:15:51 +08:00
tamakyi 1533a42620 git-sync-web 自动提交 2026-09-09 02:09:20 2026-09-09 02:09:20 +08:00
tamakyi c1765b38e3 git-sync-web 自动提交 2026-09-09 02:03:05 2026-09-09 02:03:06 +08:00
tamakyi 6946f26968 git-sync-web 自动提交 2026-09-09 01:56:49 2026-09-09 01:56:49 +08:00
tamakyi b8e653c42f git-sync-web 自动提交 2026-09-09 01:50:35 2026-09-09 01:50:35 +08:00
tamakyi 57b4199f47 git-sync-web 自动提交 2026-09-09 01:44:21 2026-09-09 01:44:21 +08:00
tamakyi 08771ad3d0 git-sync-web 自动提交 2026-09-09 01:38:05 2026-09-09 01:38:05 +08:00
tamakyi fd4990ca54 git-sync-web 自动提交 2026-09-09 01:31:50 2026-09-09 01:31:51 +08:00
tamakyi cd427c261f git-sync-web 自动提交 2026-09-09 01:25:35 2026-09-09 01:25:35 +08:00
tamakyi d05f54aa72 git-sync-web 自动提交 2026-09-09 01:19:18 2026-09-09 01:19:18 +08:00
tamakyi c03cced3b0 git-sync-web 自动提交 2026-09-09 01:13:01 2026-09-09 01:13:01 +08:00
tamakyi e813ba0f6b git-sync-web 自动提交 2026-09-09 01:06:46 2026-09-09 01:06:46 +08:00
tamakyi 82254699a8 git-sync-web 自动提交 2026-09-09 01:00:31 2026-09-09 01:00:31 +08:00
tamakyi 6c34d9cad4 git-sync-web 自动提交 2026-09-09 00:54:12 2026-09-09 00:54:12 +08:00
tamakyi 8810894374 git-sync-web 自动提交 2026-09-09 00:47:55 2026-09-09 00:47:55 +08:00
tamakyi dda75a22fe git-sync-web 自动提交 2026-09-09 00:41:40 2026-09-09 00:41:40 +08:00
tamakyi 45e997fbaa git-sync-web 自动提交 2026-09-09 00:35:24 2026-09-09 00:35:24 +08:00
tamakyi 7662c83a9f git-sync-web 自动提交 2026-09-09 00:28:53 2026-09-09 00:28:53 +08:00
tamakyi d54b029846 git-sync-web 自动提交 2026-09-09 00:22:38 2026-09-09 00:22:38 +08:00
tamakyi 4f22cc2b70 git-sync-web 自动提交 2026-09-09 00:16:23 2026-09-09 00:16:23 +08:00
tamakyi 327d28d0bf git-sync-web 自动提交 2026-09-09 00:10:06 2026-09-09 00:10:07 +08:00
tamakyi 59976614ea git-sync-web 自动提交 2026-09-09 00:03:51 2026-09-09 00:03:51 +08:00
tamakyi 8b3cd92a57 git-sync-web 自动提交 2026-09-08 23:57:33 2026-09-08 23:57:33 +08:00
tamakyi 8b178bd841 git-sync-web 自动提交 2026-09-08 23:51:18 2026-09-08 23:51:18 +08:00
tamakyi 27432f335c git-sync-web 自动提交 2026-09-08 23:45:01 2026-09-08 23:45:01 +08:00
tamakyi 1aa0c78f2f git-sync-web 自动提交 2026-09-08 23:38:44 2026-09-08 23:38:44 +08:00
tamakyi ad5aff632f git-sync-web 自动提交 2026-09-08 23:32:26 2026-09-08 23:32:26 +08:00
tamakyi 0b94ec7432 git-sync-web 自动提交 2026-09-08 23:26:12 2026-09-08 23:26:12 +08:00
tamakyi a4b2d7ec83 git-sync-web 自动提交 2026-09-08 23:19:55 2026-09-08 23:19:55 +08:00
tamakyi 6df416c1d3 git-sync-web 自动提交 2026-09-08 23:13:37 2026-09-08 23:13:38 +08:00
tamakyi e597803b71 git-sync-web 自动提交 2026-09-08 23:07:22 2026-09-08 23:07:22 +08:00
tamakyi edc1eb2ae5 git-sync-web 自动提交 2026-09-08 23:01:06 2026-09-08 23:01:06 +08:00
tamakyi 7e24cffc51 git-sync-web 自动提交 2026-09-08 22:54:53 2026-09-08 22:54:53 +08:00
tamakyi 93d0f1bf58 git-sync-web 自动提交 2026-09-08 22:48:36 2026-09-08 22:48:36 +08:00
tamakyi 17262d6d94 git-sync-web 自动提交 2026-09-08 22:42:19 2026-09-08 22:42:19 +08:00
tamakyi ff249d49ed git-sync-web 自动提交 2026-09-08 22:36:01 2026-09-08 22:36:01 +08:00
tamakyi d707132dd6 git-sync-web 自动提交 2026-09-08 22:29:42 2026-09-08 22:29:42 +08:00
tamakyi 3fc1598d61 git-sync-web 自动提交 2026-09-08 22:23:26 2026-09-08 22:23:26 +08:00
tamakyi 455e6a974c git-sync-web 自动提交 2026-09-08 22:17:09 2026-09-08 22:17:09 +08:00
tamakyi 2911eb2f07 git-sync-web 自动提交 2026-09-08 22:10:49 2026-09-08 22:10:49 +08:00
tamakyi 0bffd830a6 git-sync-web 自动提交 2026-09-08 22:04:34 2026-09-08 22:04:34 +08:00
tamakyi 518bfdaedd git-sync-web 自动提交 2026-09-08 21:58:17 2026-09-08 21:58:17 +08:00
tamakyi 33f58c38df git-sync-web 自动提交 2026-09-08 21:51:48 2026-09-08 21:51:48 +08:00
tamakyi 9f3645897b git-sync-web 自动提交 2026-09-08 21:45:30 2026-09-08 21:45:31 +08:00
tamakyi 617340dcb7 git-sync-web 自动提交 2026-09-08 21:39:15 2026-09-08 21:39:15 +08:00
tamakyi aa17f4ffc7 git-sync-web 自动提交 2026-09-08 21:32:58 2026-09-08 21:32:58 +08:00
tamakyi b335a2292f git-sync-web 自动提交 2026-09-08 21:26:42 2026-09-08 21:26:42 +08:00
tamakyi 2e42237f5c git-sync-web 自动提交 2026-09-08 21:20:18 2026-09-08 21:20:18 +08:00
tamakyi 9bd3573e75 git-sync-web 自动提交 2026-09-08 21:14:02 2026-09-08 21:14:03 +08:00
tamakyi 40665d0c80 git-sync-web 自动提交 2026-09-08 21:07:48 2026-09-08 21:07:48 +08:00
tamakyi b4ed08696c git-sync-web 自动提交 2026-09-08 21:01:32 2026-09-08 21:01:32 +08:00
tamakyi c2a90a26e1 git-sync-web 自动提交 2026-09-08 20:55:16 2026-09-08 20:55:16 +08:00
tamakyi 6ff52505e8 git-sync-web 自动提交 2026-09-08 20:48:56 2026-09-08 20:48:56 +08:00
tamakyi 9e255ed45e git-sync-web 自动提交 2026-09-08 20:42:41 2026-09-08 20:42:41 +08:00
tamakyi 7d400f0649 git-sync-web 自动提交 2026-09-08 20:36:26 2026-09-08 20:36:26 +08:00
tamakyi dbf4c6cd1f git-sync-web 自动提交 2026-09-08 20:30:10 2026-09-08 20:30:10 +08:00
tamakyi 6572fe694a git-sync-web 自动提交 2026-09-08 20:23:55 2026-09-08 20:23:55 +08:00
tamakyi 2bacccd71c git-sync-web 自动提交 2026-09-08 20:17:40 2026-09-08 20:17:41 +08:00
tamakyi 08defeeaea git-sync-web 自动提交 2026-09-08 20:11:24 2026-09-08 20:11:24 +08:00
tamakyi 469c5f6abe git-sync-web 自动提交 2026-09-08 20:05:08 2026-09-08 20:05:08 +08:00
tamakyi 064d2280e6 git-sync-web 自动提交 2026-09-08 19:58:52 2026-09-08 19:58:52 +08:00
tamakyi bac0b40066 git-sync-web 自动提交 2026-09-08 19:52:36 2026-09-08 19:52:36 +08:00
tamakyi dfa2f0f84b git-sync-web 自动提交 2026-09-08 19:46:21 2026-09-08 19:46:22 +08:00
tamakyi b38395dfe2 git-sync-web 自动提交 2026-09-08 19:40:06 2026-09-08 19:40:06 +08:00
tamakyi e9e3438b29 git-sync-web 自动提交 2026-09-08 19:33:49 2026-09-08 19:33:49 +08:00
tamakyi 68a5958750 git-sync-web 自动提交 2026-09-08 19:27:34 2026-09-08 19:27:34 +08:00
tamakyi 66d4dfd9f6 git-sync-web 自动提交 2026-09-08 19:21:15 2026-09-08 19:21:15 +08:00
tamakyi ea6a202e3c git-sync-web 自动提交 2026-09-08 19:20:31 2026-09-08 19:20:31 +08:00
tamakyi 4c262e3318 git-sync-web 自动提交 2026-09-08 19:18:07 2026-09-08 19:18:08 +08:00
tamakyi 275e6286c7 git-sync-web 自动提交 2026-09-08 19:15:03 2026-09-08 19:15:03 +08:00
tamakyi d2528f385a git-sync-web 自动提交 2026-09-08 19:14:38 2026-09-08 19:14:38 +08:00
tamakyi c4895aaac7 git-sync-web 自动提交 2026-09-08 19:14:23 2026-09-08 19:14:23 +08:00
tamakyi 26b6ff7930 fix bugs 2026-09-08 19:14:13 +08:00
tamakyi ee22cb8ad0 git-sync-web 自动提交 2026-09-08 19:13:24 2026-09-08 19:13:24 +08:00
tamakyi 85d8d33832 update engine 2026-09-08 19:06:18 +08:00
tamakyi c6c9a76d51 init 2026-09-08 18:57:46 +08:00
tamakyi 0039ed75ca git-sync-web 自动提交 2026-09-08 18:55:14 2026-09-08 18:55:15 +08:00
tamakyi ec4e32ae76 git-sync-web 自动提交 2026-09-08 18:54:01 2026-09-08 18:54:02 +08:00
tamakyi 9ad94a3ab6 git-sync-web 自动提交 2026-09-08 18:52:48 2026-09-08 18:52:49 +08:00
tamakyi 6deea58585 git-sync-web 自动提交 2026-09-08 18:51:35 2026-09-08 18:51:35 +08:00
tamakyi 2813fbcfb3 git-sync-web 自动提交 2026-09-08 18:51:25 2026-09-08 18:51:25 +08:00
tamakyi d0428b1fea git-sync-web 自动提交 2026-09-08 18:50:21 2026-09-08 18:50:21 +08:00
tamakyi 8a128af3bc git-sync-web 自动提交 2026-09-08 18:49:44 2026-09-08 18:49:44 +08:00
tamakyi c30e58dec1 git-sync-web 自动提交 2026-09-08 18:48:32 2026-09-08 18:48:32 +08:00
tamakyi 5cee3dfdbd git-sync-web 自动提交 2026-09-08 18:47:49 2026-09-08 18:47:49 +08:00
tamakyi 98cf6a64a8 git-sync-web 自动提交 2026-09-08 18:47:22 2026-09-08 18:47:22 +08:00
tamakyi cbf39e4233 git-sync-web 自动提交 2026-09-08 18:46:52 2026-09-08 18:46:52 +08:00
tamakyi d99d1ce7ab git-sync-web 自动提交 2026-09-08 18:40:57 2026-09-08 18:40:57 +08:00
tamakyi 9d3bdbe583 git-sync-web 自动提交 2026-09-08 18:34:31 2026-09-08 18:34:32 +08:00
tamakyi 2da2301c37 git-sync-web 自动提交 2026-09-08 18:28:07 2026-09-08 18:28:07 +08:00
tamakyi f5838fd8b6 git-sync-web 自动提交 2026-09-08 18:21:40 2026-09-08 18:21:41 +08:00
tamakyi f882668404 git-sync-web 自动提交 2026-09-08 18:15:10 2026-09-08 18:15:10 +08:00
tamakyi 2ea7f8563a git-sync-web 自动提交 2026-09-08 18:08:44 2026-09-08 18:08:44 +08:00
tamakyi a32dd0d444 git-sync-web 自动提交 2026-09-08 18:02:17 2026-09-08 18:02:17 +08:00
tamakyi feeeffec1f git-sync-web 自动提交 2026-09-08 17:58:20 2026-09-08 17:58:21 +08:00
tamakyi 0ac3d3e86c git-sync-web 自动提交 2026-09-08 17:55:59 2026-09-08 17:55:59 +08:00
tamakyi 03494be12f git-sync-web 自动提交 2026-09-08 17:54:08 2026-09-08 17:54:09 +08:00
tamakyi aef0b08eda git-sync-web 自动提交 2026-09-08 17:51:15 2026-09-08 17:51:16 +08:00
tamakyi 000968f4e6 git-sync-web 自动提交 2026-09-08 17:41:22 2026-09-08 17:41:23 +08:00
tamakyi c0d599dfb4 git-sync-web 自动提交 2026-09-08 17:35:44 2026-09-08 17:35:44 +08:00
tamakyi 5227d8f70c git-sync-web 自动提交 2026-09-08 17:34:26 2026-09-08 17:34:27 +08:00
tamakyi 18dbd34f2c git-sync-web 自动提交 2026-09-08 17:33:08 2026-09-08 17:33:08 +08:00
tamakyi be99e73a0e git-sync-web 自动提交 2026-09-08 17:31:52 2026-09-08 17:31:52 +08:00
tamakyi b16e589dd9 git-sync-web 自动提交 2026-09-08 17:29:31 2026-09-08 17:29:31 +08:00
tamakyi 1a92c18800 git-sync-web 自动提交 2026-09-08 17:28:15 2026-09-08 17:28:15 +08:00
tamakyi a44334d751 git-sync-web 自动提交 2026-09-08 17:26:59 2026-09-08 17:26:59 +08:00
tamakyi 06683fa1f6 git-sync-web 自动提交 2026-09-08 17:25:43 2026-09-08 17:25:44 +08:00
tamakyi a6ccd748bf git-sync-web 自动提交 2026-09-08 17:24:26 2026-09-08 17:24:27 +08:00
tamakyi 1c8e1f71d5 git-sync-web 自动提交 2026-09-08 17:23:09 2026-09-08 17:23:10 +08:00
tamakyi c57b9fa3b3 git-sync-web 自动提交 2026-09-08 17:21:52 2026-09-08 17:21:53 +08:00
tamakyi 153913f182 git-sync-web 自动提交 2026-09-08 17:20:34 2026-09-08 17:20:35 +08:00
tamakyi 90f4d90232 git-sync-web 自动提交 2026-09-08 17:19:53 2026-09-08 17:19:54 +08:00
tamakyi 076ab18599 git-sync-web 自动提交 2026-09-08 17:19:19 2026-09-08 17:19:19 +08:00
tamakyi 8e8adbd8a0 git-sync-web 自动提交 2026-09-08 17:12:27 2026-09-08 17:12:27 +08:00
35 changed files with 5220 additions and 28 deletions
+94 -9
View File
@@ -1,7 +1,13 @@
# ---> Go # Created by https://www.toptal.com/developers/gitignore/api/code,go,linux,macos,windows
# If you prefer the allow list template instead of the deny list, see community template: # Edit at https://www.toptal.com/developers/gitignore?templates=code,go,linux,macos,windows
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
# ### Code ###
.vscode/*
!.vscode/tasks.json
!.vscode/launch.json
*.code-workspace
### Go ###
# Binaries for programs and plugins # Binaries for programs and plugins
*.exe *.exe
*.exe~ *.exe~
@@ -18,10 +24,89 @@
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
# Go workspace file ### Go Patch ###
go.work /vendor/
go.work.sum /Godeps/
# env file ### Linux ###
.env *~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
#logs
logs/*
*.log
# 运行时生成的统计/日志(避免自动提交把运行产物入库)
stats/
#configs
configs/*
# End of https://www.toptal.com/developers/gitignore/api/code,go,linux,macos,windows
-18
View File
@@ -1,18 +0,0 @@
MIT License
Copyright (c) 2026 tamakyi
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
+9
View File
@@ -0,0 +1,9 @@
BINARY := git-auto.exe
# 关闭 VCS 标记:绕过本地 .git 对象库不持久导致的 build 报错
.PHONY: build run
build:
go build -buildvcs=false -o $(BINARY) .
run: build
./$(BINARY) -dir .. -addr 127.0.0.1:5757
+122 -1
View File
@@ -1,2 +1,123 @@
# github-auto-sync-golang # Git 自动同步 · 网页控制台(git-sync-web)
将 `git-auto-sync`(原 Bash 脚本版定时同步工具)的能力搬上网页:用浏览器即可监控、手动触发、查看差异、在线编辑文件、就地解决冲突,无需登录服务器敲命令行。
单二进制、纯标准库实现(Go 1.25+),前端资源通过 `go:embed` 内嵌,**零外部依赖、零运行时下载**,拷贝一个 exe 即可运行。
---
## 核心功能
### 1. 仓库总览(Dashboard)
- 每个被监控仓库一张卡片:同步状态灯、领先/落后提交数、本地改动文件数。
- 运行中的仓库实时显示;关闭「自动同步」的仓库标注「手动模式」徽标,仅响应手动触发。
- 顶部按钮「全部启动 / 全部暂停」一键管控。
### 2. 实时活动(SSE 日志)
- 右侧实时日志面板通过 Server-Sent Events 推送每个仓库的同步过程、提交、冲突等事件,并带错误/警告高亮。
- 支持按关键字过滤、一键清空。
### 3. 手动同步
- 每个仓库提供「获取 / 拉取 / 推送」按钮,随时手动触发,不必等待定时调度。
### 4. 差异查看(Diff)
- 支持「工作区 / 已暂存 / 某次提交」三种范围;统一视图与**并排(split)视图**切换。
- 可选的**词级差异**(`git --word-diff=plain`),改动以行内增删高亮呈现。
- 一键**复制**差异内容、下载 `.patch` 文件;编辑器内「对比此文件」可只看当前文件的改动。
### 5. 网页端文件编辑
- 文件树列出仓库内可跟踪 / 未跟踪文件,并标注改动状态(M/A/D/?)。
- 点击文件即可**在线读取与编辑**,保存可「仅写盘」或「保存并提交」。
- 内置 **CodeMirror** 语法高亮;编辑过程中自动缓存草稿到浏览器 `localStorage`,刷新不丢稿,可「放弃草稿」回滚。
- 二进制文件自动识别并只读提示。
### 6. 冲突解决
- **拉取冲突**:选择「放弃合并」或「我已在本地解决,继续」。
- **推送冲突**(non-fast-forward):提供「强制推送(--force-with-lease)」或「放弃」。
- **冲突文件内联编辑**:直接在网页展开冲突文件、编辑解决、保存并标记已解决;所有冲突处理完毕时自动结束合并/变基。
### 7. 自动同步 / 自动提交 / Cron
- 每个仓库可设固定间隔(秒)或 **Cron 表达式**自动拉取推送(Cron 非空时优先于固定间隔)。
- 「自动提交」开启后,检测到工作区脏文件会自动 `add -A` + `commit` 再推送。
- 「自动同步」开关可单独关闭——关闭后该仓库不再自动运行,仅响应手动同步。
### 8. 提交历史
- 浏览仓库提交记录,点击展开单条提交的详细 diff。提交哈希经正则白名单校验,防止命令注入。
### 9. 统计
- 每次同步写入 CSV 统计(领先/落后/改动等),网页端以图表呈现近 30 天趋势。
### 10. 访问密码(可选)
- 启动时加 `-password` 参数启用鉴权:生成 30 天会话 cookie,除静态资源外的 API 均受保护。
---
## 编译与运行
```bash
# 编译(会重新内嵌 static 目录下的前端资源,改前端后必须重新 build)
go build -o git-auto.exe .
# Windows下可能需要执行如下命令编译:
go build -buildvcs=false -o git-auto.exe .
# 运行
./git-auto.exe -dir "C:/OtherFiles/Github/git-auto-sync" -addr 127.0.0.1:5757
```
| 参数 | 说明 | 默认值 |
| --- | --- | --- |
| `-dir` | 项目根目录,需包含 `configs/`、`logs/`、`stats/` | `.` |
| `-addr` | 监听地址 | `127.0.0.1:5757` |
| `-password` | 访问密码(为空则不启用鉴权) | 空 |
启动后浏览器打开 `http://127.0.0.1:5757` 即可。
---
## 配置文件格式
监控目标以 `.conf` 文件存放在 `-dir` 指定的 `configs/` 目录下,兼容原 Bash 脚本的配置字段:
```ini
# Git 自动同步配置 - 示例
REPO_PATH="/c/OtherFiles/Github/your-repo" # 支持 Git Bash 风格 /c/... 路径,会自动转为 Windows 路径
REMOTE_NAME="origin"
REMOTE_URL="https://example.com/you/repo.git"
# 多远程写法:REMOTES="origin=https://a.git,fork=https://b.git"
BRANCH="main"
MODE="both" # both / push / pull
INTERVAL="60" # 固定间隔(秒)
CRON="" # 可选,cron 表达式优先于 INTERVAL
LOG_FILE="/c/.../sync.log"
AUTH_METHOD="ssh" # ssh / https
AUTO_COMMIT="0" # 1 开启自动提交
AUTO_SYNC="1" # 0 关闭自动同步(仅手动)
CONFLICT_STRATEGY="merge" # merge / rebase
PUSH_STRATEGY="manual" # manual / auto
```
> 说明:路径既支持 `C:/...` 也支持 Git Bash 的 `/c/...`,后者会在加载时自动规范化;`.git` 内部目录被禁止通过网页访问。
---
## 目录结构
```
git-sync-web/
├── main.go # 入口:-dir / -addr / -password,内嵌 static
├── config.go # .conf 解析与写回、Windows 路径规范化
├── git.go # git 执行器、领先/落后、冲突检测
├── engine.go # 每仓库调度循环、SSE 事件总线、文件树/读写
├── cron.go # 纯标准库 5 段 cron 解析(含单测)
├── server.go # REST API 与静态资源服务
└── static/ # 前端:index.html / app.js / style.css / cm-boot.js / vendor/cm/
```
---
## 设计要点
- **单二进制 + 内嵌前端**:部署只需一个可执行文件,不依赖任何外部 CDN 或运行时。
- **安全边界**:仓库内路径经 `safeRepoPath` 严格校验,禁止 `..` 越界与访问 `.git`;提交哈希白名单防注入;网页编辑单文件 ≤1MB 且拒绝二进制写入。
- **优雅降级**:CodeMirror 资源缺失或初始化失败时,编辑器自动回退原生 `<textarea>`,核心功能不受影响。
+231
View File
@@ -0,0 +1,231 @@
package main
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
type Remote struct {
Name string `json:"name"`
URL string `json:"url"`
}
type RepoConfig struct {
Name string `json:"name"`
RepoPath string `json:"repo_path"`
Branch string `json:"branch"`
Remotes []Remote `json:"remotes"`
Mode string `json:"mode"`
Interval int `json:"interval"`
LogFile string `json:"log_file"`
AuthMethod string `json:"auth_method"`
RetryCount int `json:"retry_count"`
RetryInterval int `json:"retry_interval"`
ConflictStrategy string `json:"conflict_strategy"`
PushStrategy string `json:"push_strategy"`
AutoCommit bool `json:"auto_commit"`
AutoSync bool `json:"auto_sync"`
Cron string `json:"cron"`
}
var nameRe = regexp.MustCompile(`^[A-Za-z0-9_\-]+$`)
// normalizeWinPath 把 Git Bash 风格路径 /c/foo/bar 转成 Windows 的 C:/foo/bar,
func normalizeWinPath(p string) string {
p = strings.ReplaceAll(strings.TrimSpace(p), "\\", "/")
if len(p) >= 3 && p[0] == '/' && p[2] == '/' {
d := p[1]
if (d >= 'a' && d <= 'z') || (d >= 'A' && d <= 'Z') {
return strings.ToUpper(string(d)) + ":/" + p[3:]
}
}
return p
}
func unquote(s string) string {
s = strings.TrimSpace(s)
if len(s) >= 2 {
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
return s[1 : len(s)-1]
}
}
return s
}
func orDefault(v, def string) string {
v = strings.TrimSpace(v)
if v == "" {
return def
}
return v
}
func atoiOr(v string, def int) int {
n, err := strconv.Atoi(strings.TrimSpace(v))
if err != nil {
return def
}
return n
}
func parseConfFile(path string) (map[string]string, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
m := map[string]string{}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
m[strings.TrimSpace(k)] = unquote(v)
}
return m, nil
}
func loadRepoConfig(dir, name string) (*RepoConfig, error) {
m, err := parseConfFile(filepath.Join(dir, name+".conf"))
if err != nil {
return nil, err
}
c := &RepoConfig{Name: name}
c.RepoPath = normalizeWinPath(m["REPO_PATH"])
c.Branch = orDefault(m["BRANCH"], "main")
c.Mode = orDefault(m["MODE"], "both")
c.Interval = atoiOr(m["INTERVAL"], 60)
c.LogFile = normalizeWinPath(m["LOG_FILE"])
c.AuthMethod = orDefault(m["AUTH_METHOD"], "ssh")
c.RetryCount = atoiOr(m["RETRY_COUNT"], 0)
c.RetryInterval = atoiOr(m["RETRY_INTERVAL"], 0)
c.ConflictStrategy = orDefault(m["CONFLICT_STRATEGY"], "merge")
c.PushStrategy = orDefault(m["PUSH_STRATEGY"], "manual")
c.AutoCommit = strings.EqualFold(m["AUTO_COMMIT"], "1") || strings.EqualFold(m["AUTO_COMMIT"], "true")
c.AutoSync = !(strings.EqualFold(m["AUTO_SYNC"], "0") || strings.EqualFold(m["AUTO_SYNC"], "false"))
c.Cron = strings.TrimSpace(m["CRON"])
if remotes := strings.TrimSpace(m["REMOTES"]); remotes != "" {
for _, pair := range strings.Split(remotes, ",") {
pair = strings.TrimSpace(pair)
if pair == "" {
continue
}
n, u, ok := strings.Cut(pair, "=")
if !ok {
continue
}
c.Remotes = append(c.Remotes, Remote{Name: strings.TrimSpace(n), URL: strings.TrimSpace(u)})
}
}
if len(c.Remotes) == 0 && m["REMOTE_NAME"] != "" && m["REMOTE_URL"] != "" {
c.Remotes = append(c.Remotes, Remote{Name: m["REMOTE_NAME"], URL: m["REMOTE_URL"]})
}
return c, nil
}
func q(s string) string { return "\"" + s + "\"" }
func saveRepoConfig(dir string, c *RepoConfig) error {
var b strings.Builder
fmt.Fprintf(&b, "# Git 自动同步配置 - %s\n", c.Name)
fmt.Fprintf(&b, "REPO_PATH=%s\n", q(c.RepoPath))
fmt.Fprintf(&b, "BRANCH=%s\n", q(c.Branch))
pairs := make([]string, len(c.Remotes))
for i, r := range c.Remotes {
pairs[i] = r.Name + "=" + r.URL
}
fmt.Fprintf(&b, "REMOTES=%s\n", q(strings.Join(pairs, ",")))
fmt.Fprintf(&b, "MODE=%s\n", q(c.Mode))
fmt.Fprintf(&b, "INTERVAL=%s\n", q(strconv.Itoa(c.Interval)))
fmt.Fprintf(&b, "LOG_FILE=%s\n", q(c.LogFile))
fmt.Fprintf(&b, "AUTH_METHOD=%s\n", q(c.AuthMethod))
fmt.Fprintf(&b, "RETRY_COUNT=%s\n", q(strconv.Itoa(c.RetryCount)))
fmt.Fprintf(&b, "RETRY_INTERVAL=%s\n", q(strconv.Itoa(c.RetryInterval)))
fmt.Fprintf(&b, "CONFLICT_STRATEGY=%s\n", q(c.ConflictStrategy))
fmt.Fprintf(&b, "PUSH_STRATEGY=%s\n", q(c.PushStrategy))
ac := "0"
if c.AutoCommit {
ac = "1"
}
fmt.Fprintf(&b, "AUTO_COMMIT=%s\n", q(ac))
as := "0"
if c.AutoSync {
as = "1"
}
fmt.Fprintf(&b, "AUTO_SYNC=%s\n", q(as))
fmt.Fprintf(&b, "CRON=%s\n", q(c.Cron))
return os.WriteFile(filepath.Join(dir, c.Name+".conf"), []byte(b.String()), 0o644)
}
func validateRepoConfig(cfgDir string, c *RepoConfig, isCreate bool) error {
if !nameRe.MatchString(c.Name) {
return fmt.Errorf("配置名只能包含字母、数字、下划线和连字符")
}
if isCreate {
if _, err := os.Stat(filepath.Join(cfgDir, c.Name+".conf")); err == nil {
return fmt.Errorf("配置 %q 已存在", c.Name)
}
}
p := normalizeWinPath(c.RepoPath)
if !isRepo(p) {
return fmt.Errorf("仓库路径无效或不是 Git 仓库: %s", c.RepoPath)
}
c.RepoPath = p
c.Branch = strings.TrimSpace(c.Branch)
if c.Branch == "" {
c.Branch = "main"
}
if len(c.Remotes) == 0 {
return fmt.Errorf("至少需要一个远程仓库(格式:名称=URL)")
}
for _, r := range c.Remotes {
if strings.TrimSpace(r.Name) == "" || strings.TrimSpace(r.URL) == "" {
return fmt.Errorf("远程配置格式应为 名称=URL")
}
}
switch c.Mode {
case "push", "pull", "both":
default:
return fmt.Errorf("操作模式必须是 push/pull/both")
}
switch c.AuthMethod {
case "ssh", "https":
default:
return fmt.Errorf("认证方式必须是 ssh/https")
}
switch c.ConflictStrategy {
case "merge", "rebase", "abort":
default:
return fmt.Errorf("冲突策略必须是 merge/rebase/abort")
}
switch c.PushStrategy {
case "manual", "rebase", "force":
default:
return fmt.Errorf("推送策略必须是 manual/rebase/force")
}
if c.Interval < 5 {
c.Interval = 60
}
if c.RetryCount < 0 {
c.RetryCount = 0
}
if c.RetryInterval < 0 {
c.RetryInterval = 0
}
if c.AuthMethod != "https" {
c.RetryCount, c.RetryInterval = 0, 0
}
if strings.TrimSpace(c.Cron) != "" {
if _, err := parseCron(c.Cron); err != nil {
return fmt.Errorf("cron 表达式无效: %v", err)
}
}
return nil
}
+15
View File
@@ -0,0 +1,15 @@
# Git 自动同步配置 - git-auto-sync
REPO_PATH="C:/OtherFiles/Github/git-auto-sync/git-sync-web"
BRANCH="main"
REMOTES="origin=https://git.shiroko.one/tamakyi/github-auto-sync-golang"
MODE="push"
INTERVAL="360"
LOG_FILE=""
AUTH_METHOD="https"
RETRY_COUNT="0"
RETRY_INTERVAL="0"
CONFLICT_STRATEGY="merge"
PUSH_STRATEGY="manual"
AUTO_COMMIT="0"
AUTO_SYNC="0"
CRON=""
+129
View File
@@ -0,0 +1,129 @@
package main
import (
"fmt"
"strconv"
"strings"
"time"
)
type cronSpec struct {
min, hour, dom, mon, dow map[int]bool
domAll, dowAll bool
}
func parseCron(spec string) (*cronSpec, error) {
fields := strings.Fields(spec)
if len(fields) != 5 {
return nil, fmt.Errorf("cron 需要 5 个字段(分 时 日 月 周),收到 %d 个", len(fields))
}
c := &cronSpec{}
var err error
if c.min, err = parseCronField(fields[0], 0, 59); err != nil {
return nil, fmt.Errorf("分钟字段: %w", err)
}
if c.hour, err = parseCronField(fields[1], 0, 23); err != nil {
return nil, fmt.Errorf("小时字段: %w", err)
}
if c.dom, err = parseCronField(fields[2], 1, 31); err != nil {
return nil, fmt.Errorf("日字段: %w", err)
}
if c.mon, err = parseCronField(fields[3], 1, 12); err != nil {
return nil, fmt.Errorf("月字段: %w", err)
}
if c.dow, err = parseCronField(fields[4], 0, 7); err != nil {
return nil, fmt.Errorf("星期字段: %w", err)
}
if c.dow[7] {
c.dow[0] = true
}
c.domAll = len(c.dom) == 31
c.dowAll = len(c.dow) == 8
return c, nil
}
func parseCronField(f string, min, max int) (map[int]bool, error) {
set := map[int]bool{}
for _, part := range strings.Split(f, ",") {
part = strings.TrimSpace(part)
if part == "" {
return nil, fmt.Errorf("存在空字段")
}
step := 1
if i := strings.Index(part, "/"); i >= 0 {
n, err := strconv.Atoi(part[i+1:])
if err != nil || n <= 0 {
return nil, fmt.Errorf("无效步长 %q", part)
}
step = n
part = part[:i]
if part == "" {
part = "*"
}
}
lo, hi := 0, 0
if part == "*" {
lo, hi = min, max
} else if rng := strings.SplitN(part, "-", 2); len(rng) == 2 {
var err error
if lo, err = strconv.Atoi(rng[0]); err != nil {
return nil, fmt.Errorf("无效值 %q", part)
}
if hi, err = strconv.Atoi(rng[1]); err != nil {
return nil, fmt.Errorf("无效值 %q", part)
}
} else {
v, err := strconv.Atoi(part)
if err != nil {
return nil, fmt.Errorf("无效值 %q", part)
}
lo, hi = v, v
}
if lo < min || hi > max || lo > hi {
return nil, fmt.Errorf("值超出范围 [%d,%d]: %q", min, max, part)
}
for v := lo; v <= hi; v += step {
set[v] = true
}
}
return set, nil
}
func cronNextString(expr string) (string, error) {
spec, err := parseCron(expr)
if err != nil {
return "", err
}
return cronNext(spec, time.Now()).Format("2006-01-02 15:04"), nil
}
func cronNext(spec *cronSpec, after time.Time) time.Time {
t := after.Truncate(time.Minute).Add(time.Minute)
limit := t.AddDate(1, 0, 0)
for t.Before(limit) {
if !spec.mon[int(t.Month())] {
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()).AddDate(0, 1, 0)
continue
}
domOK := spec.dom[t.Day()]
dowOK := spec.dow[int(t.Weekday())]
dayOK := domOK && dowOK
if !spec.domAll && !spec.dowAll {
dayOK = domOK || dowOK
}
if !dayOK {
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()).AddDate(0, 0, 1)
continue
}
if !spec.hour[t.Hour()] {
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location()).Add(time.Hour)
continue
}
if spec.min[t.Minute()] {
return t
}
t = t.Add(time.Minute)
}
return limit
}
+49
View File
@@ -0,0 +1,49 @@
package main
import (
"testing"
"time"
)
func TestParseCron(t *testing.T) {
for _, spec := range []string{"*/5 * * * *", "0 9 * * 1-5", "30 8 1,15 * *", "* * * * *", "0 */2 * * *", "0 8-18/2 * * 1-5"} {
if _, err := parseCron(spec); err != nil {
t.Errorf("parseCron(%q) 应当成功,却得到: %v", spec, err)
}
}
for _, spec := range []string{"* * * *", "61 * * * *", "* * * * 8", "a * * * *", "* * * * * *"} {
if _, err := parseCron(spec); err == nil {
t.Errorf("parseCron(%q) 应当失败", spec)
}
}
}
func TestCronNext(t *testing.T) {
spec, _ := parseCron("30 9 * * *")
next := cronNext(spec, time.Date(2026, 9, 8, 10, 0, 0, 0, time.Local))
want := time.Date(2026, 9, 9, 9, 30, 0, 0, time.Local)
if !next.Equal(want) {
t.Fatalf("每日 09:30:期望 %v,得到 %v", want, next)
}
spec2, _ := parseCron("*/15 * * * *")
next2 := cronNext(spec2, time.Date(2026, 9, 8, 10, 7, 0, 0, time.Local))
want2 := time.Date(2026, 9, 8, 10, 15, 0, 0, time.Local)
if !next2.Equal(want2) {
t.Fatalf("*/15:期望 %v,得到 %v", want2, next2)
}
spec3, _ := parseCron("0 12 * * 5")
next3 := cronNext(spec3, time.Date(2026, 9, 8, 12, 0, 0, 0, time.Local))
want3 := time.Date(2026, 9, 11, 12, 0, 0, 0, time.Local)
if !next3.Equal(want3) {
t.Fatalf("仅周五:期望 %v,得到 %v", want3, next3)
}
spec4, _ := parseCron("0 0 * * 7")
next4 := cronNext(spec4, time.Date(2026, 9, 12, 0, 0, 0, 0, time.Local)) // 周六
want4 := time.Date(2026, 9, 13, 0, 0, 0, 0, time.Local)
if !next4.Equal(want4) {
t.Fatalf("7=周日:期望 %v,得到 %v", want4, next4)
}
}
+1753
View File
File diff suppressed because it is too large Load Diff
+255
View File
@@ -0,0 +1,255 @@
package main
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
)
func gitRun(repoPath string, timeout time.Duration, args ...string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
full := append([]string{"-C", repoPath}, args...)
cmd := exec.CommandContext(ctx, "git", full...)
cmd.Env = append(os.Environ(),
"GIT_TERMINAL_PROMPT=0",
"GIT_ASKPASS=echo",
"GCM_INTERACTIVE=never",
"GIT_SSH_COMMAND=ssh -oBatchMode=yes -oStrictHostKeyChecking=accept-new",
)
cmd.WaitDelay = 10 * time.Second
var outBuf, errBuf bytes.Buffer
cmd.Stdout = &outBuf
cmd.Stderr = &errBuf
err := cmd.Run()
out := strings.TrimRight(outBuf.String(), "\r\n\t ")
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return out, fmt.Errorf("命令超时: git %s", strings.Join(args, " "))
}
msg := strings.TrimSpace(errBuf.String())
if msg == "" {
msg = out
}
if msg == "" {
msg = err.Error()
}
return out, errors.New(msg)
}
return out, nil
}
func isRepo(path string) bool {
if path == "" {
return false
}
_, err := os.Stat(filepath.Join(path, ".git"))
return err == nil
}
func ensureRepo(path, branch string) error {
if path == "" {
return fmt.Errorf("仓库路径为空")
}
path = normalizeWinPath(path)
if err := os.MkdirAll(path, 0o755); err != nil {
return err
}
if isRepo(path) {
return nil
}
if branch == "" {
branch = "main"
}
if _, err := gitRun(path, 30*time.Second, "init", "-b", branch); err != nil {
if _, e2 := gitRun(path, 30*time.Second, "init"); e2 != nil {
return e2
}
_, _ = gitRun(path, 15*time.Second, "checkout", "-b", branch)
}
return nil
}
func currentBranch(path string) (string, error) {
return gitRun(path, 15*time.Second, "rev-parse", "--abbrev-ref", "HEAD")
}
func aheadBehind(path, remote, branch string) (int, int, error) {
out, err := gitRun(path, 30*time.Second, "rev-list", "--left-right", "--count", "HEAD..."+remote+"/"+branch)
if err != nil {
return 0, 0, err
}
parts := strings.Fields(out)
if len(parts) != 2 {
return 0, 0, fmt.Errorf("无法解析 rev-list 输出: %q", out)
}
a, _ := strconv.Atoi(parts[0])
b, _ := strconv.Atoi(parts[1])
return a, b, nil
}
func remoteRefExists(path, remote, branch string) bool {
_, err := gitRun(path, 15*time.Second, "rev-parse", "--verify", "-q", remote+"/"+branch)
return err == nil
}
func branchExists(path, branch string) bool {
_, err := gitRun(path, 15*time.Second, "show-ref", "--verify", "-q", "refs/heads/"+branch)
return err == nil
}
func remoteTip(path, remote, branch string) (sha string, exists bool) {
if remoteRefExists(path, remote, branch) {
out, err := gitRun(path, 15*time.Second, "rev-parse", remote+"/"+branch)
if err == nil {
return strings.TrimSpace(out), true
}
}
out, err := gitRun(path, 30*time.Second, "ls-remote", remote, "refs/heads/"+branch)
if err != nil {
return "", false
}
fields := strings.Fields(strings.TrimSpace(out))
if len(fields) < 2 {
return "", false
}
return fields[0], true
}
func aheadBehindSha(path, tip string) (int, int, error) {
out, err := gitRun(path, 30*time.Second, "rev-list", "--left-right", "--count", "HEAD..."+tip)
if err != nil {
return 0, 0, err
}
parts := strings.Fields(out)
if len(parts) != 2 {
return 0, 0, fmt.Errorf("无法解析 rev-list 输出: %q", out)
}
a, _ := strconv.Atoi(parts[0])
b, _ := strconv.Atoi(parts[1])
return a, b, nil
}
func dirtyCount(path string) int {
out, err := gitRun(path, 30*time.Second, "status", "--porcelain")
if err != nil {
return 0
}
n := 0
for _, line := range strings.Split(out, "\n") {
if strings.TrimSpace(line) != "" {
n++
}
}
return n
}
var conflictCodes = map[string]bool{
"DD": true, "AU": true, "UD": true, "UA": true, "DU": true, "AA": true, "UU": true,
}
func conflictFiles(path string) ([]string, error) {
out, err := gitRun(path, 30*time.Second, "status", "--porcelain")
if err != nil {
return nil, err
}
var files []string
seen := map[string]bool{}
for _, line := range strings.Split(out, "\n") {
if len(line) < 4 {
continue
}
if !conflictCodes[line[:2]] {
continue
}
p := strings.TrimSpace(line[3:])
if i := strings.Index(p, "->"); i >= 0 {
p = strings.TrimSpace(p[i+2:])
}
if p != "" && !seen[p] {
seen[p] = true
files = append(files, p)
}
}
return files, nil
}
func abortMerge(path string) (string, error) {
if out, err := gitRun(path, 30*time.Second, "rebase", "--abort"); err == nil {
return out, nil
}
return gitRun(path, 30*time.Second, "merge", "--abort")
}
func resolveDone(path string, rebase bool) error {
if _, err := gitRun(path, 30*time.Second, "add", "-A"); err != nil {
return err
}
if rebase {
_, err := gitRun(path, 120*time.Second, "-c", "core.editor=true", "rebase", "--continue")
return err
}
if _, err := gitRun(path, 15*time.Second, "rev-parse", "-q", "--verify", "MERGE_HEAD"); err == nil {
_, err := gitRun(path, 120*time.Second, "commit", "--no-edit")
return err
}
return nil
}
func changedFiles(path string) map[string]string {
out, err := gitRun(path, 30*time.Second, "status", "--porcelain")
m := map[string]string{}
if err != nil {
return m
}
for _, line := range strings.Split(out, "\n") {
if len(line) < 4 {
continue
}
code := line[:2]
p := strings.TrimSpace(line[3:])
if i := strings.Index(p, "->"); i >= 0 {
p = strings.TrimSpace(p[i+2:])
}
p = strings.Trim(p, `"`)
if p == "" {
continue
}
switch {
case code == "??":
m[p] = "?"
case code[0] == 'A' || code[0] == 'C':
m[p] = "A"
case code[0] == 'D' || code[1] == 'D':
m[p] = "D"
default:
m[p] = "M" // 含 UU/AU 冲突状态,统一显示为已修改
}
}
for p, st := range m {
if st == "?" && strings.HasSuffix(p, "/") {
expandUntracked(path, p, m)
}
}
return m
}
func expandUntracked(repoPath, dir string, m map[string]string) {
out, err := gitRun(repoPath, 15*time.Second, "ls-files", "--others", "--exclude-standard", "--", dir)
if err != nil {
return
}
for _, ln := range strings.Split(out, "\n") {
ln = strings.TrimSpace(ln)
if ln != "" {
m[ln] = "?"
}
}
}
+1
View File
@@ -0,0 +1 @@
2026/09/09 15:59:43 Git Sync Web 已启动: http://127.0.0.1:5799(鉴权:false)
+3
View File
@@ -0,0 +1,3 @@
module git-sync-web
go 1.25
+37
View File
@@ -0,0 +1,37 @@
package main
import (
"embed"
"flag"
"io/fs"
"log"
"net/http"
)
//go:embed static
var staticFS embed.FS
func main() {
baseDir := flag.String("dir", ".", "项目根目录(包含 configs/ logs/ stats/)")
addr := flag.String("addr", "127.0.0.1:5757", "监听地址")
password := flag.String("password", "", "访问密码(为空则不启用鉴权)")
flag.Parse()
engine, err := NewEngine(*baseDir)
if err != nil {
log.Fatalf("初始化失败: %v", err)
}
engine.LoadAll()
engine.StartAll()
sub, err := fs.Sub(staticFS, "static")
if err != nil {
log.Fatalf("读取内嵌静态资源失败: %v", err)
}
srv := NewServer(engine, sub, *password)
log.Printf("Git Sync Web 已启动: http://%s(鉴权:%t)", *addr, *password != "")
if err := http.ListenAndServe(*addr, srv); err != nil {
log.Fatal(err)
}
}
+470
View File
@@ -0,0 +1,470 @@
package main
import (
"crypto/rand"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"fmt"
"io/fs"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
type Server struct {
engine *Engine
mux *http.ServeMux
password string
tokenMu sync.Mutex
tokens map[string]time.Time
}
func NewServer(e *Engine, static fs.FS, password string) *Server {
s := &Server{
engine: e,
mux: http.NewServeMux(),
password: password,
tokens: map[string]time.Time{},
}
mux := s.mux
mux.HandleFunc("GET /api/status", s.handleStatus)
mux.HandleFunc("GET /api/events", s.handleEvents)
mux.HandleFunc("POST /api/login", s.handleLogin)
mux.HandleFunc("POST /api/logout", s.handleLogout)
mux.HandleFunc("POST /api/repos", s.handleCreate)
mux.HandleFunc("GET /api/repos/{name}", s.wrapRepo(s.handleGet))
mux.HandleFunc("PUT /api/repos/{name}", s.wrapRepo(s.handleUpdate))
mux.HandleFunc("DELETE /api/repos/{name}", s.wrapRepo(s.handleDelete))
mux.HandleFunc("POST /api/repos/{name}/start", s.wrapRepo(s.startRepo))
mux.HandleFunc("POST /api/repos/{name}/stop", s.wrapRepo(s.stopRepo))
mux.HandleFunc("POST /api/repos/{name}/sync", s.wrapRepo(s.syncRepo))
mux.HandleFunc("POST /api/repos/{name}/action", s.wrapRepo(s.actionRepo))
mux.HandleFunc("POST /api/repos/{name}/force-push", s.wrapRepo(s.forcePushRepo))
mux.HandleFunc("POST /api/repos/{name}/abort-push", s.wrapRepo(s.abortPushRepo))
mux.HandleFunc("GET /api/repos/{name}/conflicts", s.wrapRepo(s.conflictsRepo))
mux.HandleFunc("GET /api/repos/{name}/log", s.wrapRepo(s.logRepo))
mux.HandleFunc("GET /api/repos/{name}/stats", s.wrapRepo(s.statsRepo))
mux.HandleFunc("GET /api/repos/{name}/commits", s.wrapRepo(s.commitsRepo))
mux.HandleFunc("GET /api/repos/{name}/commits/{hash}", s.wrapRepo(s.commitDiffRepo))
mux.HandleFunc("GET /api/repos/{name}/tree", s.wrapRepo(s.treeRepo))
mux.HandleFunc("GET /api/repos/{name}/file", s.wrapRepo(s.fileRepo))
mux.HandleFunc("PUT /api/repos/{name}/file", s.wrapRepo(s.fileWriteRepo))
mux.HandleFunc("GET /api/repos/{name}/summary", s.wrapRepo(s.summaryGetRepo))
mux.HandleFunc("POST /api/repos/{name}/summary", s.wrapRepo(s.summarySaveRepo))
mux.HandleFunc("POST /api/repos/{name}/resolve-file", s.wrapRepo(s.resolveFileRepo))
mux.HandleFunc("POST /api/repos/{name}/commit", s.wrapRepo(s.commitFileRepo))
mux.HandleFunc("GET /api/repos/{name}/diff", s.wrapRepo(s.diffRepo))
mux.HandleFunc("GET /api/cron-next", s.handleCronNext)
mux.Handle("/", http.FileServer(http.FS(static)))
return s
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if s.password != "" && strings.HasPrefix(r.URL.Path, "/api/") &&
r.URL.Path != "/api/login" && !s.validToken(r) {
writeErr(w, 401, "需要登录")
return
}
s.mux.ServeHTTP(w, r)
}
func (s *Server) validToken(r *http.Request) bool {
c, err := r.Cookie("git_sync_token")
if err != nil {
return false
}
s.tokenMu.Lock()
defer s.tokenMu.Unlock()
exp, ok := s.tokens[c.Value]
if !ok {
return false
}
if time.Now().After(exp) {
delete(s.tokens, c.Value)
return false
}
return true
}
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
var req struct {
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErr(w, 400, "请求体格式错误")
return
}
if subtle.ConstantTimeCompare([]byte(req.Password), []byte(s.password)) != 1 {
writeErr(w, 401, "密码错误")
return
}
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
writeErr(w, 500, "生成会话失败")
return
}
token := hex.EncodeToString(buf)
s.tokenMu.Lock()
s.tokens[token] = time.Now().Add(30 * 24 * time.Hour)
s.tokenMu.Unlock()
http.SetCookie(w, &http.Cookie{
Name: "git_sync_token", Value: token, Path: "/",
HttpOnly: true, MaxAge: 30 * 24 * 3600,
})
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
if c, err := r.Cookie("git_sync_token"); err == nil {
s.tokenMu.Lock()
delete(s.tokens, c.Value)
s.tokenMu.Unlock()
}
http.SetCookie(w, &http.Cookie{Name: "git_sync_token", Value: "", Path: "/", MaxAge: -1})
writeJSON(w, 200, map[string]any{"ok": true})
}
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(v)
}
func writeErr(w http.ResponseWriter, code int, msg string) {
writeJSON(w, code, map[string]string{"error": msg})
}
func (s *Server) wrapRepo(h func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
if s.engine.get(name) == nil {
writeErr(w, 404, "配置不存在: "+name)
return
}
h(w, r, name)
}
}
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, map[string]any{"repos": s.engine.Snapshots(true)})
}
func (s *Server) handleGet(w http.ResponseWriter, r *http.Request, name string) {
if st := s.engine.get(name); st != nil {
writeJSON(w, 200, st.snapshot(true))
return
}
writeErr(w, 404, "配置不存在")
}
func (s *Server) handleCreate(w http.ResponseWriter, r *http.Request) {
var cfg RepoConfig
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
writeErr(w, 400, "请求体格式错误: "+err.Error())
return
}
if err := s.engine.Create(&cfg); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true, "name": cfg.Name})
}
func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request, name string) {
var cfg RepoConfig
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
writeErr(w, 400, "请求体格式错误: "+err.Error())
return
}
cfg.Name = name
if err := s.engine.Update(&cfg); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, name string) {
if err := s.engine.Delete(name); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) startRepo(w http.ResponseWriter, r *http.Request, name string) {
if err := s.engine.Start(name); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) stopRepo(w http.ResponseWriter, r *http.Request, name string) {
if err := s.engine.Stop(name); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) syncRepo(w http.ResponseWriter, r *http.Request, name string) {
if err := s.engine.SyncNow(name); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) actionRepo(w http.ResponseWriter, r *http.Request, name string) {
var req struct {
Action string `json:"action"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErr(w, 400, "请求体格式错误")
return
}
switch req.Action {
case "abort":
if err := s.engine.ConflictAbort(name); err != nil {
writeErr(w, 500, err.Error())
return
}
case "resolve":
if err := s.engine.ConflictResolve(name); err != nil {
writeErr(w, 500, err.Error())
return
}
case "fetch", "pull", "push":
if err := s.engine.ManualGit(name, req.Action); err != nil {
writeErr(w, 500, err.Error())
return
}
default:
writeErr(w, 400, "未知操作: "+req.Action)
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) conflictsRepo(w http.ResponseWriter, r *http.Request, name string) {
writeJSON(w, 200, s.engine.ConflictDiffs(name))
}
func (s *Server) logRepo(w http.ResponseWriter, r *http.Request, name string) {
n := 300
if v, err := strconv.Atoi(r.URL.Query().Get("lines")); err == nil && v > 0 && v <= 5000 {
n = v
}
writeJSON(w, 200, map[string]any{"lines": s.engine.ReadLog(name, n)})
}
func (s *Server) statsRepo(w http.ResponseWriter, r *http.Request, name string) {
writeJSON(w, 200, s.engine.ReadStats(name))
}
func (s *Server) commitsRepo(w http.ResponseWriter, r *http.Request, name string) {
limit := 50
if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && v > 0 && v <= 200 {
limit = v
}
writeJSON(w, 200, s.engine.ReadCommits(name, limit))
}
func (s *Server) commitDiffRepo(w http.ResponseWriter, r *http.Request, name string) {
diff, err := s.engine.ReadCommitDiff(name, r.PathValue("hash"))
if err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]string{"diff": diff})
}
func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) {
fl, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("X-Accel-Buffering", "no")
id, ch := s.engine.Bus().Subscribe()
defer s.engine.Bus().Unsubscribe(id)
if snap, err := json.Marshal(map[string]any{"type": "snapshot", "data": s.engine.Snapshots(true)}); err == nil {
fmt.Fprintf(w, "data: %s\n\n", snap)
fl.Flush()
}
tick := time.NewTicker(25 * time.Second)
defer tick.Stop()
ctx := r.Context()
for {
select {
case <-ctx.Done():
return
case <-tick.C:
fmt.Fprint(w, ": ping\n\n")
fl.Flush()
case data, ok := <-ch:
if !ok {
return
}
fmt.Fprintf(w, "data: %s\n\n", data)
fl.Flush()
}
}
}
func (s *Server) forcePushRepo(w http.ResponseWriter, r *http.Request, name string) {
if err := s.engine.ForcePush(name); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) abortPushRepo(w http.ResponseWriter, r *http.Request, name string) {
if err := s.engine.AbortPush(name); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) treeRepo(w http.ResponseWriter, r *http.Request, name string) {
writeJSON(w, 200, map[string]any{"files": s.engine.FileTree(name)})
}
func (s *Server) fileRepo(w http.ResponseWriter, r *http.Request, name string) {
rel := r.URL.Query().Get("path")
if rel == "" {
writeErr(w, 400, "缺少 path 参数")
return
}
fc, err := s.engine.ReadFileContent(name, rel)
if err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, fc)
}
func (s *Server) fileWriteRepo(w http.ResponseWriter, r *http.Request, name string) {
var req struct {
Path string `json:"path"`
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErr(w, 400, "请求体格式错误")
return
}
if req.Path == "" {
writeErr(w, 400, "缺少 path")
return
}
if err := s.engine.WriteFileContent(name, req.Path, req.Content); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) summaryGetRepo(w http.ResponseWriter, r *http.Request, name string) {
writeJSON(w, 200, map[string]any{
"pending": s.engine.GetPendingSummary(name),
"log": s.engine.GetSummaryLog(name),
})
}
func (s *Server) summarySaveRepo(w http.ResponseWriter, r *http.Request, name string) {
var req struct {
Summary string `json:"summary"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErr(w, 400, "请求体格式错误")
return
}
if err := s.engine.SavePendingSummary(name, req.Summary); err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true, "pending": s.engine.GetPendingSummary(name)})
}
func (s *Server) resolveFileRepo(w http.ResponseWriter, r *http.Request, name string) {
var req struct {
Path string `json:"path"`
Content string `json:"content"`
Finish bool `json:"finish"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErr(w, 400, "请求体格式错误")
return
}
if req.Path == "" {
writeErr(w, 400, "缺少 path")
return
}
if err := s.engine.ResolveConflictFile(name, req.Path, req.Content, req.Finish); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) commitFileRepo(w http.ResponseWriter, r *http.Request, name string) {
var req struct {
Path string `json:"path"`
Message string `json:"message"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErr(w, 400, "请求体格式错误")
return
}
if req.Path == "" {
writeErr(w, 400, "缺少 path")
return
}
if err := s.engine.CommitFile(name, req.Path, req.Message); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) diffRepo(w http.ResponseWriter, r *http.Request, name string) {
scope := r.URL.Query().Get("scope")
if scope == "" {
scope = "working"
}
// mode = word 时返回 --word-diff=plain 结果(词级差异)
res, err := s.engine.WorkingDiff(name, scope, r.URL.Query().Get("path"), r.URL.Query().Get("mode"))
if err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, res)
}
func (s *Server) handleCronNext(w http.ResponseWriter, r *http.Request) {
expr := r.URL.Query().Get("expr")
if expr == "" {
writeErr(w, 400, "缺少 expr 参数")
return
}
next, err := cronNextString(expr)
if err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"next": next})
}
+1207
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
(function () {
if (typeof window.CodeMirror === 'undefined' || typeof window.Editor === 'undefined') return;
var ta = document.getElementById('file-editor');
if (!ta) return;
try {
var dark = document.documentElement.classList.contains('dark');
var cm = window.CodeMirror.fromTextArea(ta, {
lineNumbers: true,
lineWrapping: true,
indentUnit: 2,
tabSize: 2,
theme: dark ? 'material-darker' : 'eclipse',
readOnly: ta.disabled ? 'nocursor' : false,
extraKeys: {
'Ctrl-S': function () { if (window.Editor.onSave) window.Editor.onSave(); },
'Cmd-S': function () { if (window.Editor.onSave) window.Editor.onSave(); }
}
});
window.Editor.cm = cm;
cm.on('change', function () { if (window.Editor._suspend) return; if (window.Editor.onChange) window.Editor.onChange(); });
var cur = window.Editor.value;
} catch (e) {
if (window.console) console.warn('CodeMirror 初始化失败,已回退原生编辑器:', e);
}
})();
+271
View File
@@ -0,0 +1,271 @@
<!DOCTYPE html>
<html lang="zh-CN" data-page-node-id="nRvVfOmQzU2twwSAvqUwfl">
<head data-page-node-id="BuWdwjwKPwwSCpow9JSC3n">
<meta charset="utf-8" data-page-node-id="tfHCUSqYqPzGPxp06Gd1tM">
<meta name="viewport" content="width=device-width, initial-scale=1" data-page-node-id="jb078uU5LM9D2ifPdaUWAq">
<title>Git Sync 控制台</title>
<link rel="stylesheet" href="/style.css" data-page-node-id="ORLXZi9XzxrEfwCcuWSocY">
<!-- CodeMirror(本地 vendor,离线可用;浅色/深色主题均按类名隔离,可同时加载) -->
<link rel="stylesheet" href="/vendor/cm/codemirror.min.css" data-page-node-id="DEsUZn7j7yjeQYLdcwspqH">
<link rel="stylesheet" href="/vendor/cm/eclipse.css" data-page-node-id="MaPxkDtHnCAcvhei4GQ7iw">
<link rel="stylesheet" href="/vendor/cm/material-darker.css" data-page-node-id="ChKgT48SiWzjCMcWsGLrje">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
</head>
<body data-page-node-id="pZxw2WnQAZycSEktQenh8f">
<header data-page-node-id="XYY4eaCzyeDqRPq55yjUTY">
<div class="brand" data-page-node-id="xYRyIDur3LHCyDOfcx2CSX"><span class="brand-dot" data-page-node-id="GP41MDt2LXjJaYhsp8InyW"></span>Git Sync 控制台</div>
<div class="header-actions" data-page-node-id="XruBG1DkVAqNNsyOl4J41n">
<span id="conn" class="conn" data-page-node-id="aGv9c7D5e6Izpi92PjfxBz">连接中…</span>
<button class="btn btn-icon" id="btn-theme" title="切换深色 / 浅色模式" data-page-node-id="AP0nSHePHnQDDMylbBKMkA">🌙</button>
<button class="btn" id="btn-start-all" title="启动所有仓库的自动同步" data-page-node-id="QThBRVKpwwHv4WTWoGl0XA">全部启动</button>
<button class="btn" id="btn-stop-all" title="暂停所有仓库的自动同步" data-page-node-id="skWzYCenmPbVdRXS3TNxMK">全部暂停</button>
<button class="btn" id="btn-sync-all" data-page-node-id="6ag20ptiBl3F5MUA22HF68">全部同步</button>
<button class="btn btn-primary" id="btn-new" data-page-node-id="c0DooX9PlqcLzk8PGhoZR3">+ 新建配置</button>
</div>
</header>
<main class="layout" data-page-node-id="9QmwKKoxuxwUQhg1t97g3S">
<section class="main-col" data-page-node-id="sKKLeDH2PGoGGIHbEhFid9">
<div id="grid" class="grid" data-page-node-id="xoQF8aiwcrnknxwxd80TI1"></div>
<div id="empty" class="empty" hidden data-page-node-id="C97b1VODLutQfNM13MXmAQ">
<p data-page-node-id="5nTKGjtKBDEFtovt25xVkQ">还没有任何仓库配置。</p>
<button class="btn btn-primary" id="btn-empty-new" data-page-node-id="SOs5r2CqrkMlyKyIZCGjkc">创建第一个配置</button>
</div>
</section>
<aside id="live" class="live" data-page-node-id="sgJJOcfDk9vPJs2DHcddtg">
<div class="live-head" data-page-node-id="znZimXC9iQsawFUMoA0lCu">
<h2 data-page-node-id="nGI3EfHmGzvRvuwP0oie2v">实时活动</h2>
<span id="live-conn" class="conn" data-page-node-id="Kg4dQkALsqywtSpd37Zz3B">连接中…</span>
<button class="btn btn-sm" id="btn-live-clear" title="清空本面板" data-page-node-id="KmUck5XGik1k7kP9zyfV2B">清空</button>
</div>
<div class="live-hint" data-page-node-id="camBOmhyguvcYnuUJmA2E7">所有仓库的同步(手动与自动)实时显示在此,无需点进详情</div>
<div id="live-log" class="live-log" data-page-node-id="VBl8S1HvgHrCW0882nIFt2"></div>
</aside>
</main>
<div id="modal" class="modal" hidden data-page-node-id="txBcrv6ZrmZhFBqb699zeN">
<div class="modal-box" data-page-node-id="KdrABdnmnNUg8UQdCa7Nqv">
<div class="modal-head" data-page-node-id="QdJ8BjaSvq7DxthECgP16z">
<h2 id="m-title" data-page-node-id="5kDCaJqI0QPGtb0QllYLBm">详情</h2>
<button class="btn-icon" id="m-close" title="关闭" data-page-node-id="cEUcru162HY4bHMBMNTEfI">✕</button>
</div>
<div class="tabs" id="m-tabs" data-page-node-id="CuT5FO4bado4BoZKGRXLGA">
<button data-tab="log" class="tab active" data-page-node-id="UirzBTtYe9llMyNeM4Q7cS">日志</button>
<button data-tab="history" class="tab" data-page-node-id="CPGPmAmMmERkIKBuv5Dw20">历史</button>
<button data-tab="diff" class="tab" data-page-node-id="hxChwjabjboAjspxC9VQyT">差异</button>
<button data-tab="file" class="tab" data-page-node-id="TotQXXPG6HhUSfeSm8XHz2">文件</button>
<button data-tab="conflict" class="tab" data-page-node-id="ujZu5YIEdpocOLfXLYhPv8">冲突</button>
<button data-tab="stats" class="tab" data-page-node-id="iIUiptYinU7iEa6VFcmceV">统计</button>
<button data-tab="config" class="tab" data-page-node-id="LRRFTfdian98kzNy3ca7U3">配置</button>
</div>
<div class="tab-body" data-page-node-id="BrgjnFG38oxkdJfBGHBRIX">
<div id="tab-log" class="tab-pane" data-page-node-id="LeZ5xD39VJIgWzOgMDLmji">
<div class="log-tools" data-page-node-id="VeZ2yA8bXiB5QkTnVxho88">
<label data-page-node-id="mu1MsfnHFce2PblMBUHuBz">级别
<select id="log-level" data-page-node-id="qRmCfGUzlRUbqzPfRd3GMb">
<option value="all" data-page-node-id="MBqUhLyyEFwF5KlLC2KzZq">全部</option>
<option value="error" data-page-node-id="PAJzGYoj7CRpKDjFKGgRF9">仅错误</option>
<option value="warn" data-page-node-id="m58wjzleIydsdSo49RQDUn">仅警告</option>
<option value="info" data-page-node-id="AGIjFdNqVxkFMzLjdbM76q">仅信息</option>
</select>
</label>
<input id="log-search" class="log-search" placeholder="搜索日志内容(关键词)" data-page-node-id="diPbIGZMP5CvkxPAGMsEnR">
<button class="btn btn-sm" id="btn-log-clear" title="清除过滤条件" data-page-node-id="WTCY4f4B9KEUbcx59P7S1K">清除</button>
</div>
<pre id="logbox" class="logbox" data-page-node-id="xXJ1Cc3SnDycFy75rRr8FY">加载中…</pre>
</div>
<div id="tab-history" class="tab-pane" hidden data-page-node-id="StRYtav5tS7RxMlmHkgMSV">
<div id="commit-list" class="commit-list" data-page-node-id="vLBDjSJ9KvjCVXYgjXFluN">加载中…</div>
</div>
<div id="tab-diff" class="tab-pane" hidden data-page-node-id="8ZEuDbkKtSFB1H2vNCD76h">
<div class="diff-head" data-page-node-id="pXqbBCb7m3PfpYSKGgGBJX">
<label data-page-node-id="9logygXL6fEhQpwTrZDwHR">范围
<select id="f-diff-scope" data-page-node-id="tjqhjYveDmQDIx4B9g1B58">
<option value="working" data-page-node-id="hmF9tmY9YCefzG7ppCmi7K">工作区(未暂存)</option>
<option value="staged" data-page-node-id="O5a6sN0qQaMZQPE6tGLqhU">暂存区</option>
<option value="all" data-page-node-id="xCr8XjV5khXje91R7F0axX">全部(含已提交未推送)</option>
<option value="remote" data-page-node-id="YWBratpqmpHwx7GuFLa2cC">与远程对比</option>
</select>
</label>
<label data-page-node-id="HM7TUuaIIeWw55c6H6p8et">视图
<select id="f-diff-view" data-page-node-id="pPFl4XiZHHV6OW02F6izDl">
<option value="unified" data-page-node-id="UBjVfLe9r8IoW6n4Br722d">统一</option>
<option value="split" data-page-node-id="52oaT6kkwDaWCwPJgQesYV">并排</option>
</select>
</label>
<label class="check-inline" title="按单词而非整行显示改动" data-page-node-id="Tn8hpGEewmZoD4cEfS4KUV"><input type="checkbox" id="f-diff-word" data-page-node-id="lxIM4byqutKFfAsBwujha6"> 词级</label>
<button class="btn btn-sm" id="btn-diff-refresh" data-page-node-id="3nrxtZxQJZHYXHAE7NR1Qq">刷新</button>
<button class="btn btn-sm" id="btn-diff-copy" data-page-node-id="oNXn145DgiaPDu4Aw4mOxG">复制</button>
<button class="btn btn-sm" id="btn-diff-download" data-page-node-id="ibtJzrmQVfhLYTIWAdCeqI">下载 .patch</button>
<span id="diff-filter" class="diff-filter" hidden data-page-node-id="XcHdGYotv6fgDrzsjgMerW"></span>
</div>
<div id="diff-stat" class="diff-stat" data-page-node-id="2hoANSTC9oMHaGMrEQ4HIl"></div>
<pre id="diffbox" class="diffbox" data-page-node-id="19VEbYBBwIIk0rsVl1M5FX">加载中…</pre>
</div>
<div id="tab-file" class="tab-pane" hidden data-page-node-id="kAFseOkCuTSGBdvb4EmQZd">
<div class="file-head" data-page-node-id="UjmP651Cok2T5VcpTyNEEA">
<button class="btn btn-sm" id="btn-file-refresh" data-page-node-id="RF7EO9SpUEx62Gxu2GDfnJ">刷新文件列表</button>
<button class="btn btn-sm" id="btn-file-diff" title="只看当前文件的差异" data-page-node-id="4EiZjZ2rCfX5ZqD5ACDd7U">对比此文件</button>
<span id="file-current" class="file-current" data-page-node-id="Y29ei6OnYN1W2AWCe6Jueg"></span>
<span id="file-draft" class="file-draft" hidden data-page-node-id="gQRdJvI800DgkQ6h6trjuL"></span>
</div>
<div class="file-body" data-page-node-id="RXmkN5Ep4iegYWG3iWBiON">
<div id="file-list" class="file-list" data-page-node-id="F75SFyDQShim9rZFPnjuJC">加载中…</div>
<div class="file-edit-col" data-page-node-id="xh1qou2SVboDfvKTLcXFEA">
<div class="file-chg-hint" id="file-chg-hint" hidden>● 黄色高亮 = 相对最近一次提交仍未提交的修改行</div>
<textarea id="file-editor" class="file-editor" placeholder="选择左侧文件进行编辑(Ctrl/Cmd + S 保存)" spellcheck="false"></textarea>
<div class="file-actions" data-page-node-id="GAMAnkYKeMi93EVEx8eYOn">
<input id="file-msg" class="file-msg" placeholder="提交说明(可选)" data-page-node-id="aS2jRAr1kAkqElk1T4Cldz">
<button class="btn btn-sm" id="btn-file-save" data-page-node-id="gaVGyD6YkFFElkXJFBtLk5">保存</button>
<button class="btn btn-sm" id="btn-file-commit-only" title="提交当前文件在磁盘上的改动(不含编辑器里尚未保存的修改)">提交</button>
<button class="btn btn-sm btn-primary" id="btn-file-commit" data-page-node-id="1VDju8pdiboyM0zBgwff21">保存并提交</button>
</div>
</div>
</div>
</div>
<div id="tab-conflict" class="tab-pane" hidden data-page-node-id="EFoL9Iv3JY4DBXK2ovqVl3">
<div class="manual-actions" data-page-node-id="OSsThrKzK22Q1tIcZ0a2bu">
<button class="btn btn-sm" id="btn-fetch" data-page-node-id="JEWw9Jx1x6PWh5EzLd7Bp9">获取</button>
<button class="btn btn-sm" id="btn-pull" data-page-node-id="D3F7c7ZkTj2GaQqrBtshfW">拉取</button>
<button class="btn btn-sm" id="btn-push" data-page-node-id="qTA9vsiTSuWz4dwilu7Prf">推送</button>
</div>
<div class="conflict-actions" id="pull-conflict-actions" data-page-node-id="u1mYEBVApFghZDqR2RFg7V">
<button class="btn btn-sm btn-danger" id="btn-abort" data-page-node-id="iFRU44FOA3NXieww4PmbAR">放弃合并</button>
<button class="btn btn-sm btn-primary" id="btn-resolved" data-page-node-id="C4SMNurpXaOJ8jByGjS2zg">我已在本地解决,继续</button>
</div>
<div class="conflict-actions" id="push-conflict-actions" hidden data-page-node-id="C2kukXRbyT5HAgGznQBEOF">
<span class="hint" id="push-conflict-hint" data-page-node-id="meqqutN8Y8BlHoIUOsDuc3"></span>
<button class="btn btn-sm btn-danger" id="btn-force-push" data-page-node-id="20FtTVRLlB3HrObTPUgS7I">强制推送(--force-with-lease)</button>
<button class="btn btn-sm" id="btn-abort-push" data-page-node-id="kr5vGtshzwTYewuRpjenau">放弃强制推送</button>
</div>
<div id="conflict-list" data-page-node-id="5cossjGefHTRs2BQmv7ZiE"></div>
</div>
<div id="tab-stats" class="tab-pane" hidden data-page-node-id="AmEsRLuCU7oz0ILjoFOSan">
<div id="stat-summary" class="stat-summary" data-page-node-id="B3x3zXCqPMHxf9Sbo22JsT"></div>
<canvas id="stat-chart" height="120" data-page-node-id="IzH3GbmZh2ogX04vsMxXz5"></canvas>
</div>
<div id="tab-config" class="tab-pane" hidden data-page-node-id="Q0lO9KCy3xEzAkMXn5oQoc">
<form id="cfg-form" autocomplete="off" data-page-node-id="02yHvyuCTFoEYiXQz2Dvv5">
<div class="form-grid" data-page-node-id="SXw7lpnaPt4PKbbHGpCIJT">
<label data-page-node-id="qyzGnJE1Gt2AFnpJFtmmWe">配置名称
<input name="name" id="f-name" required pattern="[A-Za-z0-9_\-]+" title="仅字母、数字、下划线、连字符" data-page-node-id="NF2A817ZBdsM4puREMglkE">
</label>
<label data-page-node-id="QC3jvGyY6uBpIUe0dtLRyy">分支
<input name="branch" id="f-branch" value="main" data-page-node-id="uIDYoqF9rtqJ0kqijuojOd">
</label>
<label class="full" data-page-node-id="AEwFVqzONKMsh1CjmIWTfB">仓库路径
<input name="repo_path" id="f-path" placeholder="C:\path\to\repo 或 /c/path/to/repo" required data-page-node-id="V8Tqs5XjcAN6ezkffD7A0A">
</label>
<label class="full" data-page-node-id="SZ5RWmnnD0vgIfUoyNcCAP">远程仓库(每行一个,格式 名称=URL)
<textarea name="remotes" id="f-remotes" rows="3" placeholder="origin=https://github.com/user/repo.git"></textarea>
</label>
<label data-page-node-id="QUBANIDAtqHXOLlp2iHfUw">操作模式
<select name="mode" id="f-mode" data-page-node-id="PYGTzl4KFnOy5QLTnl9bil">
<option value="both" data-page-node-id="4uWdIiNUcKH1klnQC7Yyfk">both(拉取并推送)</option>
<option value="push" data-page-node-id="Fx5PMaVHG9UGFzmyKtNoVC">push(仅推送)</option>
<option value="pull" data-page-node-id="g0oRcyHrhBbgOpG3htuVkZ">pull(仅拉取)</option>
</select>
</label>
<label data-page-node-id="SR3Eyg3vhv2DEOD4ux6HbU">检查间隔(秒)
<input name="interval" id="f-interval" type="number" min="5" value="60" data-page-node-id="t4WQ8TOUFADFvrxWDqDLOO">
</label>
<label data-page-node-id="pVjnoOnEBz06wQUDFuGnSG">认证方式
<select name="auth_method" id="f-auth" data-page-node-id="fsjBIeHacR0Y68FPjYrlIE">
<option value="ssh" data-page-node-id="D1rhiol1YcEJjSQptztATh">SSH</option>
<option value="https" data-page-node-id="SC5nYAdz2JBiHIks7TFSaN">HTTPS(支持重试)</option>
</select>
</label>
<label data-page-node-id="HzJHn4COhFnLGwn6aIiBKP">冲突策略
<select name="conflict_strategy" id="f-conflict" data-page-node-id="tJK7z7t0ojAK0xkHaM80nd">
<option value="merge" data-page-node-id="BdiprVQi0oHS9aTDD4LQ2l">merge(冲突后等待人工解决)</option>
<option value="rebase" data-page-node-id="jCngOAxfELckp4A3mrk1qI">rebase(变基)</option>
<option value="abort" data-page-node-id="aGj68eybADJcGx5G1g85UB">abort(冲突后自动放弃)</option>
</select>
</label>
<label data-page-node-id="gwR8fc4feWQMTOGVirMjCO">推送策略(non-fast-forward)
<select name="push_strategy" id="f-push" data-page-node-id="HCS9CZyBErr1VeYSJxKStW">
<option value="manual" data-page-node-id="yurv3dbmJx7LKFDzzvzIUP">manual(冲突后等待人工决策)</option>
<option value="rebase" data-page-node-id="E3ifgThEynHNJhBnLn0a88">rebase(变基到远程后重试)</option>
<option value="force" data-page-node-id="lhFHAEDyn49ABT02QDys8o">force(--force-with-lease 强制推送)</option>
</select>
</label>
<label data-page-node-id="Nv2HjS10oANWojhUmKVQE4">重试次数(HTTPS)
<input name="retry_count" id="f-retry" type="number" min="0" value="3" data-page-node-id="UEgQOQBxHxqIfR5kYyZctp">
</label>
<label data-page-node-id="jNiYqEK7BpWhx0mg0CvdDY">重试间隔(秒,HTTPS)
<input name="retry_interval" id="f-retry-interval" type="number" min="0" value="5" data-page-node-id="sFEylqamHk8vW9H2D0alce">
</label>
<label class="full check-row" data-page-node-id="gWPFS7fXyat6FiuobDoynQ">
<input type="checkbox" id="f-autocommit" data-page-node-id="5ADMjJXNR5EGPDmhwiCBlm">
<span data-page-node-id="z0tpoI6jpAbJCEwUJ4cSxO">自动提交模式:检测到未提交改动时自动 commit,并随同步推送到远程</span>
</label>
<label class="full check-row">
<input type="checkbox" id="f-autosync">
<span>自动同步:按间隔 / Cron 自动拉取推送。关闭后仅响应「立即同步」手动触发,不再自动运行</span>
</label>
<label class="full" data-page-node-id="tei066TFL8N9sqInjysFvD">Cron 表达式(可选,5 段:分 时 日 月 周;填写后优先于固定间隔)
<input name="cron" id="f-cron" placeholder="*/30 * * * * 表示每 30 分钟,留空使用固定间隔" data-page-node-id="6W3Ua8pPoGLuaNh7rGcDOw">
<span id="cron-next" class="cron-next" data-page-node-id="WgXuBnZPqGWvOBGRxzIVCn"></span>
</label>
<label class="full" data-page-node-id="fsjZ6wLid5NUBV0yAHKdII">日志文件路径(留空使用默认 logs/配置名.log)
<input name="log_file" id="f-log" data-page-node-id="KJpv7jEWYnOQZ1PtQMetfP">
</label>
<div class="summary-box" id="summary-box" hidden>
<div class="summary-head">
<span>变更摘要(本地,不同步)</span>
<button type="button" class="btn btn-sm" id="btn-summary-save">保存摘要</button>
</div>
<textarea id="f-summary" class="file-editor" style="min-height:84px" placeholder="填写后将作为【自动提交】的下一次 commit 信息;留空则由工具自动生成一句话并在本地记录。此摘要与映射仅存于本地 summaries/,不会上传。"></textarea>
<div class="summary-log" id="summary-log"></div>
</div>
</div>
<div class="form-actions" data-page-node-id="1GfIJLKdxSyPwBqpqCCMb9">
<button type="submit" class="btn btn-primary" data-page-node-id="E2123htNE6lmoNzZTg7AwV">保存</button>
<button type="button" class="btn" id="btn-clone" hidden data-page-node-id="H6BdDiraff2cjD9EWjF8Eh">克隆此配置</button>
<button type="button" class="btn btn-danger" id="btn-delete" hidden data-page-node-id="iyYF2JZ87tE2mGPCkbHX5w">删除配置</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="login" class="modal" hidden data-page-node-id="bALcA8zunmLBixpK2bI55B">
<div class="modal-box login-box" data-page-node-id="kcGN8ZqtOQx7KUIgGpoGCN">
<div class="modal-head" data-page-node-id="TPAEpzey0DyD3tbzQcX6AD">
<h2 data-page-node-id="SxFYrLjYGUJGks4LhmWFGa">Git Sync 控制台</h2>
</div>
<div class="login-body" data-page-node-id="loM04avwKu3ehaXbaPV1dc">
<label data-page-node-id="PQlrAv5lABcwv1V9A2kAZf">访问密码
<input type="password" id="login-password" data-page-node-id="BQQm1tLuMFSt6HhCgtUSzi">
</label>
<p id="login-err" class="login-err" hidden data-page-node-id="c5KlyY0GNpRJlp2ZCDLC2P"></p>
<div class="form-actions" data-page-node-id="dKo7nNYzQBz8MQZa7x51Bf">
<button class="btn btn-primary" id="btn-login" data-page-node-id="EOo505vSrX2QIGGOnpcDxp">登录</button>
</div>
</div>
</div>
</div>
<!-- Toast 轻提示容器(替代原生 alert,固定在右下角,不打断操作) -->
<div id="toasts" class="toasts" data-page-node-id="sGCNypYh5YHNDe7nZ7TUeA"></div>
<script src="/app.js"></script>
<!-- CodeMirror:本地 vendor,必须在 app.js 之后加载以便降级检测 -->
<script src="/vendor/cm/codemirror.min.js"></script>
<script src="/vendor/cm/mode/javascript.min.js"></script>
<script src="/vendor/cm/mode/xml.min.js"></script>
<script src="/vendor/cm/mode/css.min.js"></script>
<script src="/vendor/cm/mode/htmlmixed.min.js"></script>
<script src="/vendor/cm/mode/go.min.js"></script>
<script src="/vendor/cm/mode/markdown.min.js"></script>
<script src="/vendor/cm/mode/shell.min.js"></script>
<script src="/vendor/cm/mode/python.min.js"></script>
<script src="/vendor/cm/mode/clike.min.js"></script>
<script src="/vendor/cm/mode/yaml.min.js"></script>
<script src="/vendor/cm/mode/sql.min.js"></script>
<script src="/vendor/cm/mode/properties.min.js"></script>
<script src="/vendor/cm/mode/diff.min.js"></script>
<script src="/cm-boot.js"></script>
</body>
</html>
+271
View File
@@ -0,0 +1,271 @@
:root {
--bg: #f6f7f9; --card: #fff; --border: #e3e5e8; --text: #1f2328; --muted: #6a737d;
--blue: #185fa5; --green: #3b6d11; --green-bg: #eaf3de; --red: #a32d2d; --red-bg: #fcebeb;
--amber: #854f0b; --amber-bg: #faeeda; --gray-bg: #f1efe8; --radius: 12px;
}
* { box-sizing: border-box; }
body { margin: 0; font: 14px/1.6 -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; background: var(--bg); color: var(--text); }
header { display: flex; justify-content: space-between; align-items: center; padding: 12px 24px; background: #fff; border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 10; }
.brand { font-weight: 500; font-size: 16px; display: flex; align-items: center; gap: 8px; }
.brand-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--blue); display: inline-block; }
.header-actions { display: flex; gap: 8px; align-items: center; }
.conn { font-size: 12px; color: var(--muted); margin-right: 8px; }
.conn.on { color: var(--green); }
.conn.off { color: var(--red); }
.btn { border: 1px solid var(--border); background: #fff; border-radius: 8px; padding: 6px 14px; font-size: 13px; cursor: pointer; color: var(--text); }
.btn:hover { border-color: #b6bcc4; background: #f3f4f6; }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn-primary { background: var(--blue); border-color: var(--blue); color: #fff; }
.btn-primary:hover { background: #124d86; border-color: #124d86; }
.btn-danger { background: #fff; border-color: #e2b6b6; color: var(--red); }
.btn-danger:hover { background: var(--red-bg); }
.btn-icon { border: none; background: none; font-size: 18px; cursor: pointer; color: var(--muted); }
main { max-width: 1320px; margin: 18px auto; padding: 0 18px; }
.layout { display: flex; gap: 16px; align-items: flex-start; }
.main-col { flex: 1; min-width: 0; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
.empty { text-align: center; color: var(--muted); padding: 80px 0; }
/* 全局实时活动面板 */
.live { width: 460px; flex: none; position: sticky; top: 78px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); display: flex; flex-direction: column; max-height: calc(100vh - 100px); }
.live-head { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--border); }
.live-head h2 { margin: 0; font-size: 15px; font-weight: 500; flex: 1; }
.live-head .conn { margin: 0; }
.btn-sm { border: 1px solid var(--border); background: #fff; border-radius: 7px; padding: 3px 10px; font-size: 12px; cursor: pointer; color: var(--muted); }
.btn-sm:hover { border-color: #b6bcc4; background: #f3f4f6; }
.live-hint { font-size: 11px; color: var(--muted); padding: 8px 14px; border-bottom: 1px solid var(--border); }
.live-log { flex: 1; overflow-y: auto; padding: 8px 10px; font-family: Consolas, "Cascadia Code", monospace; font-size: 12px; line-height: 1.45; }
.live-line { display: flex; gap: 6px; padding: 2px 0; border-bottom: 1px solid #f2f2f2; }
.live-line .ll-time { color: #9aa0a6; flex: none; }
.live-line .ll-repo { color: var(--blue); flex: none; font-weight: 600; max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.live-line .ll-msg { color: var(--text); min-width: 0; word-break: break-word; }
.live-line.lv-err .ll-msg { color: var(--red); }
.live-line.lv-warn .ll-msg { color: var(--amber); }
.live-line.lv-err .ll-repo, .live-line.lv-warn .ll-repo { color: #b06a00; }
.last-detail { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 980px) {
.layout { flex-direction: column; }
.live { width: 100%; position: static; max-height: 360px; }
}
.card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; display: flex; flex-direction: column; gap: 6px; }
.card.st-conflict { border-color: #e8c98a; background: #fffdf6; }
.card.st-error { border-color: #eec9c9; }
.card-head { display: flex; align-items: center; gap: 8px; }
.card-head h3 { margin: 0; font-size: 15px; font-weight: 500; flex: 1; }
.dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }
.st-ok .dot { background: #4f9e33; }
.st-error .dot { background: #d34b4b; }
.st-conflict .dot { background: #e39a1d; }
.st-idle .dot { background: #b4b2a9; }
.st-running .dot { background: #378add; animation: pulse 1.2s infinite; }
@keyframes pulse { 50% { opacity: .35; } }
.mode { font-size: 11px; color: var(--muted); border: 1px solid var(--border); padding: 1px 6px; border-radius: 6px; }
.path { font-size: 12px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: Consolas, monospace; }
.badges { display: flex; gap: 6px; flex-wrap: wrap; }
.badge { font-size: 12px; background: var(--gray-bg); border-radius: 6px; padding: 1px 8px; color: #444; }
.badge.b-up { background: var(--green-bg); color: var(--green); }
.badge.b-down { background: #e6f1fb; color: var(--blue); }
.badge.b-dirty { background: var(--amber-bg); color: var(--amber); }
.badge.b-conflict { background: var(--red-bg); color: var(--red); }
.last { font-size: 12px; color: var(--muted); }
.err { font-size: 12px; color: var(--red); background: var(--red-bg); border-radius: 8px; padding: 6px 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card-btns { display: flex; gap: 8px; margin-top: auto; }
.modal { position: fixed; inset: 0; background: rgba(0,0,0,.35); display: flex; align-items: center; justify-content: center; z-index: 100; }
.modal[hidden] { display: none; }
.modal-box { background: #fff; border-radius: 16px; width: min(860px, 94vw); max-height: 88vh; display: flex; flex-direction: column; overflow: hidden; }
.modal-head { display: flex; justify-content: space-between; align-items: center; padding: 14px 20px; border-bottom: 1px solid var(--border); }
.modal-head h2 { margin: 0; font-size: 16px; font-weight: 500; }
.tabs { display: flex; gap: 4px; padding: 8px 20px 0; border-bottom: 1px solid var(--border); }
.tab { border: none; background: none; padding: 8px 14px; font-size: 13px; cursor: pointer; color: var(--muted); border-bottom: 2px solid transparent; }
.tab.active { color: var(--blue); border-bottom-color: var(--blue); }
.tab-body { padding: 16px 20px; overflow: auto; }
.tab-pane[hidden] { display: none; }
.logbox { margin: 0; background: #f6f7f9; border: 1px solid var(--border); padding: 12px; border-radius: 8px; font: 12px/1.5 Consolas, monospace; white-space: pre-wrap; word-break: break-all; max-height: 55vh; overflow: auto; }
.conflict-actions { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
.manual-actions { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
.conflict-file { margin-bottom: 14px; }
.conflict-file h4 { margin: 0 0 6px; font-size: 13px; font-family: Consolas, monospace; }
.conflict-file pre { margin: 0; background: #f6f7f9; border: 1px solid var(--border); border-radius: 8px; padding: 10px; font: 12px/1.5 Consolas, monospace; white-space: pre-wrap; word-break: break-all; max-height: 300px; overflow: auto; }
.stat-summary { display: flex; gap: 16px; margin-bottom: 12px; font-size: 13px; color: var(--muted); flex-wrap: wrap; }
.stat-summary b { color: var(--text); }
.commit-list { max-height: 55vh; overflow: auto; }
.commit { border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; overflow: hidden; }
.commit-head { display: flex; gap: 10px; align-items: baseline; padding: 8px 12px; cursor: pointer; }
.commit-head:hover { background: #f6f7f9; }
.commit-hash { font: 12px Consolas, monospace; color: var(--blue); flex: none; }
.commit-subject { flex: 1; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.commit-meta { font-size: 12px; color: var(--muted); flex: none; }
.commit-diff { margin: 0; border-top: 1px solid var(--border); background: #f6f7f9; padding: 10px; font: 12px/1.5 Consolas, monospace; white-space: pre-wrap; word-break: break-all; max-height: 320px; overflow: auto; }
.commit-diff[hidden] { display: none; }
.login-box { width: min(360px, 92vw); }
.login-body { padding: 20px; display: flex; flex-direction: column; gap: 10px; }
.login-err { color: var(--red); font-size: 12px; margin: 0; }
.form-grid label.check-row { flex-direction: row; align-items: center; gap: 8px; font-size: 13px; color: var(--text); }
.form-grid label.check-row input { width: auto; margin: 0; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; }
.form-grid label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted); }
.form-grid .full { grid-column: 1 / -1; }
input, select, textarea { border: 1px solid var(--border); border-radius: 8px; padding: 7px 10px; font-size: 13px; font-family: inherit; color: var(--text); background: #fff; }
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--blue); }
textarea { font-family: Consolas, monospace; resize: vertical; }
.form-actions { display: flex; gap: 8px; margin-top: 16px; }
@media (max-width: 640px) { .form-grid { grid-template-columns: 1fr; } }
/* ---------- 差异显示(网页端) ---------- */
.diff-head { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; flex-wrap: wrap; }
.diff-head label { font-size: 12px; color: var(--muted); display: flex; gap: 6px; align-items: center; }
.diff-stat { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; font-size: 12px; }
.diff-stat .df { background: var(--gray-bg); border-radius: 6px; padding: 1px 8px; font-family: Consolas, monospace; }
.diff-stat .df.df-add { background: var(--green-bg); }
.diff-stat .df.df-del { background: var(--red-bg); }
.diffbox { margin: 0; background: #f6f7f9; border: 1px solid var(--border); padding: 10px; border-radius: 8px; font: 12px/1.5 Consolas, monospace; white-space: pre; word-break: break-all; max-height: 56vh; overflow: auto; }
.diff-line { white-space: pre; }
.diff-line.diff-add { background: var(--green-bg); color: var(--green); }
.diff-line.diff-del { background: var(--red-bg); color: var(--red); }
.diff-line.diff-hunk { color: var(--blue); background: #eef3f8; }
/* ---------- 文件编辑(网页端) ---------- */
.file-head { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; flex-wrap: wrap; }
.file-current { font-size: 12px; color: var(--muted); font-family: Consolas, monospace; }
.file-body { display: flex; gap: 14px; align-items: stretch; min-height: 420px; }
.file-list { flex: none; width: 280px; overflow: auto; border: 1px solid var(--border); border-radius: 8px; padding: 6px; background: #fff; max-height: 56vh; }
.file-list:empty::after { content: '(无文件)'; color: var(--muted); font-size: 12px; padding: 4px; }
.file-item { padding: 4px 8px; border-radius: 6px; font-size: 12.5px; font-family: Consolas, monospace; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-item:hover { background: #f0f3f6; }
.file-edit-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
.file-chg-hint { font-size: 12px; color: #9a6b00; background: rgba(255, 196, 0, 0.12); border: 1px solid rgba(224, 160, 32, 0.4); border-radius: 6px; padding: 4px 8px; }
html.dark .file-chg-hint { color: #d9a93a; background: rgba(255, 196, 0, 0.1); border-color: rgba(201, 143, 28, 0.4); }
.file-editor { flex: 1; min-height: 360px; width: 100%; border: 1px solid var(--border); border-radius: 8px; padding: 10px; font: 12.5px/1.55 Consolas, monospace; color: var(--text); background: #fff; white-space: pre; overflow: auto; }
.file-editor:disabled { background: #f6f7f9; color: var(--muted); }
.file-actions { display: flex; gap: 8px; align-items: center; }
.file-msg { flex: 1; }
.summary-box { margin-top: 16px; padding: 12px 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--card); }
.summary-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 13px; color: var(--muted); }
.summary-head .btn-sm { padding: 4px 10px; font-size: 12px; }
.summary-log { margin-top: 10px; padding: 10px 12px; border: 1px dashed var(--border); border-radius: 8px; background: #fafbfc; font: 12px/1.6 Consolas, "Microsoft YaHei", monospace; color: var(--text); white-space: pre-wrap; max-height: 220px; overflow: auto; }
html.dark .summary-log { background: #12151a; }
@media (max-width: 760px) {
.file-body { flex-direction: column; }
.file-list { width: 100%; max-height: 200px; }
}
/* ---------- 暗色模式 ---------- */
html.dark {
--bg: #16181c; --card: #1e2127; --border: #30363d; --text: #d8dee6; --muted: #8b949e;
--blue: #58a6ff; --green: #7ee787; --green-bg: #12261a; --red: #ff7b72; --red-bg: #2d1618;
--amber: #e3b341; --amber-bg: #2b2313; --gray-bg: #24272e;
}
html.dark header { background: var(--card); }
html.dark .btn { background: var(--card); }
html.dark .btn:hover { background: var(--gray-bg); border-color: #4b535d; }
html.dark .btn-sm { background: var(--card); color: var(--text); }
html.dark .btn-danger { background: var(--card); }
html.dark .btn-primary { background: var(--blue); border-color: var(--blue); color: #0d1117; }
html.dark .btn-primary:hover { background: #3d8bfd; border-color: #3d8bfd; }
html.dark .logbox,
html.dark .conflict-file pre,
html.dark .commit-diff,
html.dark .diffbox,
html.dark .file-editor:disabled { background: #12151a; }
html.dark .commit-head:hover { background: var(--gray-bg); }
html.dark .diff-line.diff-hunk { background: #17242f; }
html.dark input, html.dark select, html.dark textarea { background: #12151a; }
html.dark .file-list { background: var(--card); }
html.dark .file-item:hover { background: var(--gray-bg); }
html.dark .file-editor { background: #12151a; }
/* ---------- 日志过滤 ---------- */
.log-tools { display: flex; gap: 10px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
.log-tools label { font-size: 12px; color: var(--muted); display: flex; gap: 6px; align-items: center; }
.log-search { flex: 1; min-width: 140px; }
/* ---------- 差异:单文件过滤提示 ---------- */
.diff-filter { font-size: 12px; color: var(--amber); background: var(--amber-bg); border-radius: 6px; padding: 2px 8px; }
.diff-filter[hidden] { display: none; }
/* ---------- 文件编辑:草稿提示与状态徽标 ---------- */
.file-draft { font-size: 12px; color: var(--amber); background: var(--amber-bg); border-radius: 6px; padding: 2px 8px; }
.file-draft[hidden] { display: none; }
.file-item { display: flex; align-items: center; gap: 6px; }
.file-item .fi-name { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.fi-status { flex: none; font-size: 11px; padding: 0 5px; border-radius: 4px; background: var(--gray-bg); color: var(--muted); }
.fi-status.s-M { background: var(--amber-bg); color: var(--amber); }
.fi-status.s-A { background: var(--green-bg); color: var(--green); }
.fi-status.s-D { background: var(--red-bg); color: var(--red); }
.fi-status.s-Q { background: var(--gray-bg); color: var(--muted); }
/* ---------- 卡片:久未同步 ---------- */
.badge.b-stale { background: var(--amber-bg); color: var(--amber); }
.badge.b-paused { background: var(--gray-bg); color: var(--muted); }
/* ---------- 配置:cron 下次执行预览 ---------- */
.cron-next { font-size: 12px; color: var(--muted); min-height: 16px; }
.cron-next.bad { color: var(--red); }
.cron-next.good { color: var(--green); }
/* ---------- Toast 轻提示(替代 alert) ---------- */
.toasts { position: fixed; right: 20px; bottom: 20px; display: flex; flex-direction: column; gap: 8px; z-index: 9999; pointer-events: none; }
.toast { pointer-events: auto; cursor: pointer; max-width: 380px; padding: 10px 14px; border-radius: 8px; font-size: 13px; color: #fff; background: #333; box-shadow: 0 6px 18px rgba(0, 0, 0, .18); animation: toast-in .22s ease-out; }
.toast.t-info { background: #2f6feb; }
.toast.t-success { background: #2da44e; }
.toast.t-warn { background: #bf8700; }
.toast.t-error { background: #cf222e; }
.toast.out { opacity: 0; transform: translateY(6px); transition: all .3s; }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
/* ---------- 差异:并排视图 ---------- */
.diff-file-head { font: 12px Consolas, monospace; color: var(--muted); padding: 4px 6px; background: var(--gray-bg); border-radius: 6px; margin: 8px 0 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.srow { display: flex; border-top: 1px solid var(--border); }
.srow .scol { flex: 1; min-width: 0; display: flex; gap: 8px; padding: 0 6px; font: 12px/1.5 Consolas, monospace; white-space: pre-wrap; word-break: break-all; }
.srow .sl { border-right: 1px solid var(--border); }
.srow .ln { flex: none; width: 44px; text-align: right; color: var(--muted); user-select: none; opacity: .75; }
.srow .stxt { flex: 1; }
.srow.s-del .sl { background: var(--red-bg); color: var(--red); }
.srow.s-add .sr { background: var(--green-bg); color: var(--green); }
.srow.s-chg .sl { background: var(--red-bg); color: var(--red); }
.srow.s-chg .sr { background: var(--green-bg); color: var(--green); }
/* ---------- 差异:词级高亮 ---------- */
.wd-add { background: var(--green-bg); color: var(--green); border-radius: 3px; }
.wd-del { background: var(--red-bg); color: var(--red); border-radius: 3px; text-decoration: line-through; }
.diff-head label.check-inline { flex-direction: row; align-items: center; gap: 4px; }
.diff-head label.check-inline input { width: auto; margin: 0; }
/* ---------- CodeMirror 适配 ---------- */
.CodeMirror { height: 420px; border: 1px solid var(--border); border-radius: 8px; font: 12.5px/1.55 Consolas, monospace; }
.CodeMirror-scroll { min-height: 380px; }
/* 未提交修改行高亮 */
.CodeMirror-linebackground.cm-chg {
background-color: rgba(255, 196, 0, 0.16);
box-shadow: inset 3px 0 0 0 #e0a020;
}
html.dark .CodeMirror-linebackground.cm-chg {
background-color: rgba(255, 196, 0, 0.13);
box-shadow: inset 3px 0 0 0 #c98f1c;
}
/* ---------- 冲突文件内联编辑 ---------- */
.cf-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; flex-wrap: wrap; }
.cf-head h4 { margin: 0; flex: 1; }
.cf-ta { width: 100%; min-height: 260px; border: 1px solid var(--border); border-radius: 8px; padding: 10px; font: 12.5px/1.55 Consolas, monospace; color: var(--text); background: #fff; white-space: pre; overflow: auto; }
html.dark .cf-ta { background: #12151a; }
.cf-actions { display: flex; gap: 8px; margin-top: 6px; flex-wrap: wrap; }
.cf-hint { font-size: 12px; color: var(--muted); margin-top: 6px; line-height: 1.5; }
.cf-edit { margin-bottom: 10px; }
.conflict-file { border: 1px solid var(--border); border-radius: 8px; padding: 10px; margin-bottom: 10px; }
.conflict-file pre { margin: 0; }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+23
View File
@@ -0,0 +1,23 @@
.cm-s-eclipse span.cm-meta { color: #FF1717; }
.cm-s-eclipse span.cm-keyword { line-height: 1em; font-weight: bold; color: #7F0055; }
.cm-s-eclipse span.cm-atom { color: #219; }
.cm-s-eclipse span.cm-number { color: #164; }
.cm-s-eclipse span.cm-def { color: #00f; }
.cm-s-eclipse span.cm-variable { color: black; }
.cm-s-eclipse span.cm-variable-2 { color: #0000C0; }
.cm-s-eclipse span.cm-variable-3, .cm-s-eclipse span.cm-type { color: #0000C0; }
.cm-s-eclipse span.cm-property { color: black; }
.cm-s-eclipse span.cm-operator { color: black; }
.cm-s-eclipse span.cm-comment { color: #3F7F5F; }
.cm-s-eclipse span.cm-string { color: #2A00FF; }
.cm-s-eclipse span.cm-string-2 { color: #f50; }
.cm-s-eclipse span.cm-qualifier { color: #555; }
.cm-s-eclipse span.cm-builtin { color: #30a; }
.cm-s-eclipse span.cm-bracket { color: #cc7; }
.cm-s-eclipse span.cm-tag { color: #170; }
.cm-s-eclipse span.cm-attribute { color: #00c; }
.cm-s-eclipse span.cm-link { color: #219; }
.cm-s-eclipse span.cm-error { color: #f00; }
.cm-s-eclipse .CodeMirror-activeline-background { background: #e8f2ff; }
.cm-s-eclipse .CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; }
+135
View File
@@ -0,0 +1,135 @@
/*
Name: material
Author: Mattia Astorino (http://github.com/equinusocio)
Website: https://material-theme.site/
*/
.cm-s-material-darker.CodeMirror {
background-color: #212121;
color: #EEFFFF;
}
.cm-s-material-darker .CodeMirror-gutters {
background: #212121;
color: #545454;
border: none;
}
.cm-s-material-darker .CodeMirror-guttermarker,
.cm-s-material-darker .CodeMirror-guttermarker-subtle,
.cm-s-material-darker .CodeMirror-linenumber {
color: #545454;
}
.cm-s-material-darker .CodeMirror-cursor {
border-left: 1px solid #FFCC00;
}
.cm-s-material-darker div.CodeMirror-selected {
background: rgba(97, 97, 97, 0.2);
}
.cm-s-material-darker.CodeMirror-focused div.CodeMirror-selected {
background: rgba(97, 97, 97, 0.2);
}
.cm-s-material-darker .CodeMirror-line::selection,
.cm-s-material-darker .CodeMirror-line>span::selection,
.cm-s-material-darker .CodeMirror-line>span>span::selection {
background: rgba(128, 203, 196, 0.2);
}
.cm-s-material-darker .CodeMirror-line::-moz-selection,
.cm-s-material-darker .CodeMirror-line>span::-moz-selection,
.cm-s-material-darker .CodeMirror-line>span>span::-moz-selection {
background: rgba(128, 203, 196, 0.2);
}
.cm-s-material-darker .CodeMirror-activeline-background {
background: rgba(0, 0, 0, 0.5);
}
.cm-s-material-darker .cm-keyword {
color: #C792EA;
}
.cm-s-material-darker .cm-operator {
color: #89DDFF;
}
.cm-s-material-darker .cm-variable-2 {
color: #EEFFFF;
}
.cm-s-material-darker .cm-variable-3,
.cm-s-material-darker .cm-type {
color: #f07178;
}
.cm-s-material-darker .cm-builtin {
color: #FFCB6B;
}
.cm-s-material-darker .cm-atom {
color: #F78C6C;
}
.cm-s-material-darker .cm-number {
color: #FF5370;
}
.cm-s-material-darker .cm-def {
color: #82AAFF;
}
.cm-s-material-darker .cm-string {
color: #C3E88D;
}
.cm-s-material-darker .cm-string-2 {
color: #f07178;
}
.cm-s-material-darker .cm-comment {
color: #545454;
}
.cm-s-material-darker .cm-variable {
color: #f07178;
}
.cm-s-material-darker .cm-tag {
color: #FF5370;
}
.cm-s-material-darker .cm-meta {
color: #FFCB6B;
}
.cm-s-material-darker .cm-attribute {
color: #C792EA;
}
.cm-s-material-darker .cm-property {
color: #C792EA;
}
.cm-s-material-darker .cm-qualifier {
color: #DECB6B;
}
.cm-s-material-darker .cm-variable-3,
.cm-s-material-darker .cm-type {
color: #DECB6B;
}
.cm-s-material-darker .cm-error {
color: rgba(255, 255, 255, 1.0);
background-color: #FF5370;
}
.cm-s-material-darker .CodeMirror-matchingbracket {
text-decoration: underline;
color: white !important;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
/**
* Minified by jsDelivr using Terser v5.39.0.
* Original file: /npm/[email protected]/mode/diff/diff.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}((function(e){"use strict";e.defineMode("diff",(function(){var e={"+":"positive","-":"negative","@":"meta"};return{token:function(r){var i=r.string.search(/[\t ]+?$/);if(!r.sol()||0===i)return r.skipToEnd(),("error "+(e[r.string.charAt(0)]||"")).replace(/ $/,"");var o=e[r.peek()]||r.skipToEnd();return-1===i?r.skipToEnd():r.pos=i,o}}})),e.defineMIME("text/x-diff","diff")}));
//# sourceMappingURL=/sm/8d9c85fa00786d92f88ffec12c7faecc40483fd8269ff974fc6a9ac45ff24a83.map
+8
View File
@@ -0,0 +1,8 @@
/**
* Minified by jsDelivr using Terser v5.39.0.
* Original file: /npm/[email protected]/mode/go/go.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}((function(e){"use strict";e.defineMode("go",(function(t){var n,r=t.indentUnit,i={break:!0,case:!0,chan:!0,const:!0,continue:!0,default:!0,defer:!0,else:!0,fallthrough:!0,for:!0,func:!0,go:!0,goto:!0,if:!0,import:!0,interface:!0,map:!0,package:!0,range:!0,return:!0,select:!0,struct:!0,switch:!0,type:!0,var:!0,bool:!0,byte:!0,complex64:!0,complex128:!0,float32:!0,float64:!0,int8:!0,int16:!0,int32:!0,int64:!0,string:!0,uint8:!0,uint16:!0,uint32:!0,uint64:!0,int:!0,uint:!0,uintptr:!0,error:!0,rune:!0,any:!0,comparable:!0},o={true:!0,false:!0,iota:!0,nil:!0,append:!0,cap:!0,close:!0,complex:!0,copy:!0,delete:!0,imag:!0,len:!0,make:!0,new:!0,panic:!0,print:!0,println:!0,real:!0,recover:!0},a=/[+\-*&^%:=<>!|\/]/;function c(e,t){var r,l=e.next();if('"'==l||"'"==l||"`"==l)return t.tokenize=(r=l,function(e,t){for(var n,i=!1,o=!1;null!=(n=e.next());){if(n==r&&!i){o=!0;break}i=!i&&"`"!=r&&"\\"==n}return(o||!i&&"`"!=r)&&(t.tokenize=c),"string"}),t.tokenize(e,t);if(/[\d\.]/.test(l))return"."==l?e.match(/^[0-9_]+([eE][\-+]?[0-9_]+)?/):"0"==l?e.match(/^[xX][0-9a-fA-F_]+/)||e.match(/^[0-7_]+/):e.match(/^[0-9_]*\.?[0-9_]*([eE][\-+]?[0-9_]+)?/),"number";if(/[\[\]{}\(\),;\:\.]/.test(l))return n=l,null;if("/"==l){if(e.eat("*"))return t.tokenize=u,u(e,t);if(e.eat("/"))return e.skipToEnd(),"comment"}if(a.test(l))return e.eatWhile(a),"operator";e.eatWhile(/[\w\$_\xa1-\uffff]/);var f=e.current();return i.propertyIsEnumerable(f)?("case"!=f&&"default"!=f||(n="case"),"keyword"):o.propertyIsEnumerable(f)?"atom":"variable"}function u(e,t){for(var n,r=!1;n=e.next();){if("/"==n&&r){t.tokenize=c;break}r="*"==n}return"comment"}function l(e,t,n,r,i){this.indented=e,this.column=t,this.type=n,this.align=r,this.prev=i}function f(e,t,n){return e.context=new l(e.indented,t,n,null,e.context)}function s(e){if(e.context.prev){var t=e.context.type;return")"!=t&&"]"!=t&&"}"!=t||(e.indented=e.context.indented),e.context=e.context.prev}}return{startState:function(e){return{tokenize:null,context:new l((e||0)-r,0,"top",!1),indented:0,startOfLine:!0}},token:function(e,t){var r=t.context;if(e.sol()&&(null==r.align&&(r.align=!1),t.indented=e.indentation(),t.startOfLine=!0,"case"==r.type&&(r.type="}")),e.eatSpace())return null;n=null;var i=(t.tokenize||c)(e,t);return"comment"==i||(null==r.align&&(r.align=!0),"{"==n?f(t,e.column(),"}"):"["==n?f(t,e.column(),"]"):"("==n?f(t,e.column(),")"):"case"==n?r.type="case":("}"==n&&"}"==r.type||n==r.type)&&s(t),t.startOfLine=!1),i},indent:function(t,n){if(t.tokenize!=c&&null!=t.tokenize)return e.Pass;var i=t.context,o=n&&n.charAt(0);if("case"==i.type&&/^(?:case|default)\b/.test(n))return t.context.type="}",i.indented;var a=o==i.type;return i.align?i.column+(a?0:1):i.indented+(a?0:r)},electricChars:"{}):",closeBrackets:"()[]{}''\"\"``",fold:"brace",blockCommentStart:"/*",blockCommentEnd:"*/",lineComment:"//"}})),e.defineMIME("text/x-go","go")}));
//# sourceMappingURL=/sm/afb31834b17dab66dd28a8c66be3854cfcbeada5f859d586a76e7e1dae598035.map
+8
View File
@@ -0,0 +1,8 @@
/**
* Minified by jsDelivr using Terser v5.37.0.
* Original file: /npm/[email protected]/mode/htmlmixed/htmlmixed.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror"),require("../xml/xml"),require("../javascript/javascript"),require("../css/css")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../xml/xml","../javascript/javascript","../css/css"],t):t(CodeMirror)}((function(t){"use strict";var e={script:[["lang",/(javascript|babel)/i,"javascript"],["type",/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i,"javascript"],["type",/./,"text/plain"],[null,null,"javascript"]],style:[["lang",/^css$/i,"css"],["type",/^(text\/)?(x-)?(stylesheet|css)$/i,"css"],["type",/./,"text/plain"],[null,null,"css"]]};var a={};function n(t,e){var n=t.match(function(t){var e=a[t];return e||(a[t]=new RegExp("\\s+"+t+"\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*"))}(e));return n?/^\s*(.*?)\s*$/.exec(n[2])[1]:""}function l(t,e){return new RegExp((e?"^":"")+"</\\s*"+t+"\\s*>","i")}function r(t,e){for(var a in t)for(var n=e[a]||(e[a]=[]),l=t[a],r=l.length-1;r>=0;r--)n.unshift(l[r])}t.defineMode("htmlmixed",(function(a,o){var i=t.getMode(a,{name:"xml",htmlMode:!0,multilineTagIndentFactor:o.multilineTagIndentFactor,multilineTagIndentPastTag:o.multilineTagIndentPastTag,allowMissingTagName:o.allowMissingTagName}),c={},s=o&&o.tags,u=o&&o.scriptTypes;if(r(e,c),s&&r(s,c),u)for(var m=u.length-1;m>=0;m--)c.script.unshift(["type",u[m].matches,u[m].mode]);function d(e,r){var o,s=i.token(e,r.htmlState),u=/\btag\b/.test(s);if(u&&!/[<>\s\/]/.test(e.current())&&(o=r.htmlState.tagName&&r.htmlState.tagName.toLowerCase())&&c.hasOwnProperty(o))r.inTag=o+" ";else if(r.inTag&&u&&/>$/.test(e.current())){var m=/^([\S]+) (.*)/.exec(r.inTag);r.inTag=null;var f=">"==e.current()&&function(t,e){for(var a=0;a<t.length;a++){var l=t[a];if(!l[0]||l[1].test(n(e,l[0])))return l[2]}}(c[m[1]],m[2]),g=t.getMode(a,f),p=l(m[1],!0),h=l(m[1],!1);r.token=function(t,e){return t.match(p,!1)?(e.token=d,e.localState=e.localMode=null,null):function(t,e,a){var n=t.current(),l=n.search(e);return l>-1?t.backUp(n.length-l):n.match(/<\/?$/)&&(t.backUp(n.length),t.match(e,!1)||t.match(n)),a}(t,h,e.localMode.token(t,e.localState))},r.localMode=g,r.localState=t.startState(g,i.indent(r.htmlState,"",""))}else r.inTag&&(r.inTag+=e.current(),e.eol()&&(r.inTag+=" "));return s}return{startState:function(){return{token:d,inTag:null,localMode:null,localState:null,htmlState:t.startState(i)}},copyState:function(e){var a;return e.localState&&(a=t.copyState(e.localMode,e.localState)),{token:e.token,inTag:e.inTag,localMode:e.localMode,localState:a,htmlState:t.copyState(i,e.htmlState)}},token:function(t,e){return e.token(t,e)},indent:function(e,a,n){return!e.localMode||/^\s*<\//.test(a)?i.indent(e.htmlState,a,n):e.localMode.indent?e.localMode.indent(e.localState,a,n):t.Pass},innerMode:function(t){return{state:t.localState||t.htmlState,mode:t.localMode||i}}}}),"xml","javascript","css"),t.defineMIME("text/html","htmlmixed")}));
//# sourceMappingURL=/sm/e2c7ccb5e794d8515250b328c4947e850defb14ee2247c63cd3549e4879addb0.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
/**
* Minified by jsDelivr using Terser v5.39.0.
* Original file: /npm/[email protected]/mode/properties/properties.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}((function(e){"use strict";e.defineMode("properties",(function(){return{token:function(e,i){var t=e.sol()||i.afterSection,n=e.eol();if(i.afterSection=!1,t&&(i.nextMultiline?(i.inMultiline=!0,i.nextMultiline=!1):i.position="def"),n&&!i.nextMultiline&&(i.inMultiline=!1,i.position="def"),t)for(;e.eatSpace(););var o=e.next();return!t||"#"!==o&&"!"!==o&&";"!==o?t&&"["===o?(i.afterSection=!0,e.skipTo("]"),e.eat("]"),"header"):"="===o||":"===o?(i.position="quote",null):("\\"===o&&"quote"===i.position&&e.eol()&&(i.nextMultiline=!0),i.position):(i.position="comment",e.skipToEnd(),"comment")},startState:function(){return{position:"def",nextMultiline:!1,inMultiline:!1,afterSection:!1}}}})),e.defineMIME("text/x-properties","properties"),e.defineMIME("text/x-ini","properties")}));
//# sourceMappingURL=/sm/d7f5d26900daa33cef63eebc8dc4bdbc427e9b294e0f4cf802b0c5e3334df8f6.map
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
/**
* Minified by jsDelivr using Terser v5.39.0.
* Original file: /npm/[email protected]/mode/shell/shell.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}((function(e){"use strict";e.defineMode("shell",(function(){var t={};function n(e,n){for(var r=0;r<n.length;r++)t[n[r]]=e}var r=["true","false"],i=["if","then","do","else","elif","while","until","for","in","esac","fi","fin","fil","done","exit","set","unset","export","function"],o=["ab","awk","bash","beep","cat","cc","cd","chown","chmod","chroot","clear","cp","curl","cut","diff","echo","find","gawk","gcc","get","git","grep","hg","kill","killall","ln","ls","make","mkdir","openssl","mv","nc","nl","node","npm","ping","ps","restart","rm","rmdir","sed","service","sh","shopt","shred","source","sort","sleep","ssh","start","stop","su","sudo","svn","tee","telnet","top","touch","vi","vim","wall","wc","wget","who","write","yes","zsh"];function s(e,n){if(e.eatSpace())return null;var r,i=e.sol(),o=e.next();if("\\"===o)return e.next(),null;if("'"===o||'"'===o||"`"===o)return n.tokens.unshift(u(o,"`"===o?"quote":"string")),c(e,n);if("#"===o)return i&&e.eat("!")?(e.skipToEnd(),"meta"):(e.skipToEnd(),"comment");if("$"===o)return n.tokens.unshift(l),c(e,n);if("+"===o||"="===o)return"operator";if("-"===o)return e.eat("-"),e.eatWhile(/\w/),"attribute";if("<"==o){if(e.match("<<"))return"operator";var s=e.match(/^<-?\s*['"]?([^'"]*)['"]?/);if(s)return n.tokens.unshift((r=s[1],function(e,t){return e.sol()&&e.string==r&&t.tokens.shift(),e.skipToEnd(),"string-2"})),"string-2"}if(/\d/.test(o)&&(e.eatWhile(/\d/),e.eol()||!/\w/.test(e.peek())))return"number";e.eatWhile(/[\w-]/);var f=e.current();return"="===e.peek()&&/\w+/.test(f)?"def":t.hasOwnProperty(f)?t[f]:null}function u(e,t){var n="("==e?")":"{"==e?"}":e;return function(r,i){for(var o,s=!1;null!=(o=r.next());){if(o===n&&!s){i.tokens.shift();break}if("$"===o&&!s&&"'"!==e&&r.peek()!=n){s=!0,r.backUp(1),i.tokens.unshift(l);break}if(!s&&e!==n&&o===e)return i.tokens.unshift(u(e,t)),c(r,i);if(!s&&/['"]/.test(o)&&!/['"]/.test(e)){i.tokens.unshift(f(o,"string")),r.backUp(1);break}s=!s&&"\\"===o}return t}}function f(e,t){return function(n,r){return r.tokens[0]=u(e,t),n.next(),c(n,r)}}e.registerHelper("hintWords","shell",r.concat(i,o)),n("atom",r),n("keyword",i),n("builtin",o);var l=function(e,t){t.tokens.length>1&&e.eat("$");var n=e.next();return/['"({]/.test(n)?(t.tokens[0]=u(n,"("==n?"quote":"{"==n?"def":"string"),c(e,t)):(/\d/.test(n)||e.eatWhile(/\w/),t.tokens.shift(),"def")};function c(e,t){return(t.tokens[0]||s)(e,t)}return{startState:function(){return{tokens:[]}},token:function(e,t){return c(e,t)},closeBrackets:"()[]{}''\"\"``",lineComment:"#",fold:"brace"}})),e.defineMIME("text/x-sh","shell"),e.defineMIME("application/x-sh","shell")}));
//# sourceMappingURL=/sm/6326f7536a83bb931d36bf06f21e2f1cde4cdedfae18cc24abc8472357b9ecf0.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
/**
* Minified by jsDelivr using Terser v5.39.0.
* Original file: /npm/[email protected]/mode/yaml/yaml.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}((function(e){"use strict";e.defineMode("yaml",(function(){var e=new RegExp("\\b(("+["true","false","on","off","yes","no"].join(")|(")+"))$","i");return{token:function(i,t){var r=i.peek(),n=t.escaped;if(t.escaped=!1,"#"==r&&(0==i.pos||/\s/.test(i.string.charAt(i.pos-1))))return i.skipToEnd(),"comment";if(i.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/))return"string";if(t.literal&&i.indentation()>t.keyCol)return i.skipToEnd(),"string";if(t.literal&&(t.literal=!1),i.sol()){if(t.keyCol=0,t.pair=!1,t.pairStart=!1,i.match("---"))return"def";if(i.match("..."))return"def";if(i.match(/\s*-\s+/))return"meta"}if(i.match(/^(\{|\}|\[|\])/))return"{"==r?t.inlinePairs++:"}"==r?t.inlinePairs--:"["==r?t.inlineList++:t.inlineList--,"meta";if(t.inlineList>0&&!n&&","==r)return i.next(),"meta";if(t.inlinePairs>0&&!n&&","==r)return t.keyCol=0,t.pair=!1,t.pairStart=!1,i.next(),"meta";if(t.pairStart){if(i.match(/^\s*(\||\>)\s*/))return t.literal=!0,"meta";if(i.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i))return"variable-2";if(0==t.inlinePairs&&i.match(/^\s*-?[0-9\.\,]+\s?$/))return"number";if(t.inlinePairs>0&&i.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/))return"number";if(i.match(e))return"keyword"}return!t.pair&&i.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^\s,\[\]{}#&*!|>'"%@`])[^#:]*(?=:($|\s))/)?(t.pair=!0,t.keyCol=i.indentation(),"atom"):t.pair&&i.match(/^:\s*/)?(t.pairStart=!0,"meta"):(t.pairStart=!1,t.escaped="\\"==r,i.next(),null)},startState:function(){return{pair:!1,pairStart:!1,keyCol:0,inlinePairs:0,inlineList:0,literal:!1,escaped:!1}},lineComment:"#",fold:"indent"}})),e.defineMIME("text/x-yaml","yaml"),e.defineMIME("text/yaml","yaml")}));
//# sourceMappingURL=/sm/bc6554c36ed3032264accb3f8afb06468bccf6317bd6643e836095b9a02eabf3.map