Squashed commit of the following:

commit a2cc0e4b8b
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Tue Jun 11 07:13:32 2024 +0200

    console

commit 537026cdf2
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Tue Jun 11 07:09:16 2024 +0200

    warning

commit 0af14a0b8f
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Tue Jun 11 06:59:24 2024 +0200

    remixd disable

commit c5915b0ad6
Merge: 7f1c2b6ad 3964e4b3a
Author: Your Name <you@example.com>
Date:   Mon Jun 10 17:08:56 2024 +0200

    Merge branch 'master' of https://github.com/ethereum/remix-project into git4refactor2

commit 7f1c2b6adb
Author: Your Name <you@example.com>
Date:   Mon Jun 10 16:55:27 2024 +0200

    UploadToGists

commit cc78028f36
Author: Your Name <you@example.com>
Date:   Mon Jun 10 16:54:28 2024 +0200

    flaky

commit 7c9dff36c9
Author: Your Name <you@example.com>
Date:   Mon Jun 10 16:45:08 2024 +0200

    flaky

commit e940a616c6
Author: Your Name <you@example.com>
Date:   Mon Jun 10 16:32:22 2024 +0200

    add pause

commit 18290370bf
Author: Your Name <you@example.com>
Date:   Mon Jun 10 16:26:13 2024 +0200

    vyper test

commit 11f70933b2
Author: Your Name <you@example.com>
Date:   Mon Jun 10 16:13:14 2024 +0200

    workspace tests

commit c7d991257b
Author: Your Name <you@example.com>
Date:   Mon Jun 10 15:36:16 2024 +0200

    lint

commit 089e9cdf5d
Author: Your Name <you@example.com>
Date:   Mon Jun 10 15:35:50 2024 +0200

    remotes test

commit 357a889076
Author: Your Name <you@example.com>
Date:   Mon Jun 10 14:58:36 2024 +0200

    workspace test

commit cb98efeebd
Author: Your Name <you@example.com>
Date:   Mon Jun 10 14:56:59 2024 +0200

    fix upstream

commit ec5e66e32a
Author: Your Name <you@example.com>
Date:   Mon Jun 10 14:37:23 2024 +0200

    workspace test

commit d29472da6b
Author: Your Name <you@example.com>
Date:   Mon Jun 10 14:13:15 2024 +0200

    fp test

commit c290436216
Author: Your Name <you@example.com>
Date:   Mon Jun 10 14:04:23 2024 +0200

    linting

commit 870c260a40
Author: Your Name <you@example.com>
Date:   Mon Jun 10 14:03:44 2024 +0200

    add test

commit ef2014c3e6
Author: Your Name <you@example.com>
Date:   Mon Jun 10 13:53:33 2024 +0200

    fix undo

commit 36a16b812a
Author: Your Name <you@example.com>
Date:   Mon Jun 10 12:31:44 2024 +0200

    commands test

commit 21b3e732ef
Author: Your Name <you@example.com>
Date:   Mon Jun 10 12:12:42 2024 +0200

    upstream

commit 02567607e1
Author: Your Name <you@example.com>
Date:   Mon Jun 10 12:03:58 2024 +0200

    flaky

commit b62c54bbe1
Author: Your Name <you@example.com>
Date:   Mon Jun 10 11:43:05 2024 +0200

    remote test

commit 918a1662a4
Author: Your Name <you@example.com>
Date:   Mon Jun 10 10:11:07 2024 +0200

    github tests

commit 2f9a0b8005
Author: Your Name <you@example.com>
Date:   Mon Jun 10 09:00:18 2024 +0200

    lint

commit 1af49cb949
Author: Your Name <you@example.com>
Date:   Mon Jun 10 08:52:12 2024 +0200

    branch testing

commit 5de0383552
Author: Your Name <you@example.com>
Date:   Sun Jun 9 19:21:32 2024 +0200

    lint

commit d769014d60
Author: Your Name <you@example.com>
Date:   Sun Jun 9 19:04:19 2024 +0200

    branch testing

commit fb9c7f57b2
Author: Your Name <you@example.com>
Date:   Sun Jun 9 18:49:46 2024 +0200

    branch test

commit 2e7af0d4f5
Author: Your Name <you@example.com>
Date:   Sun Jun 9 18:26:00 2024 +0200

    more testing

commit 61090a9c65
Author: Your Name <you@example.com>
Date:   Sun Jun 9 15:40:12 2024 +0200

    change data-id

commit b6438d0623
Author: Your Name <you@example.com>
Date:   Sun Jun 9 15:38:14 2024 +0200

    lint

commit 57cfec6516
Author: Your Name <you@example.com>
Date:   Sun Jun 9 15:29:15 2024 +0200

    expand test

commit 33e90077d3
Author: Your Name <you@example.com>
Date:   Sun Jun 9 14:37:56 2024 +0200

    add file test

commit 97802c9a63
Author: Your Name <you@example.com>
Date:   Sun Jun 9 14:14:03 2024 +0200

    add test

commit 087dd2c262
Author: Your Name <you@example.com>
Date:   Sun Jun 9 13:37:35 2024 +0200

    fix proxy

commit c0497fbd4d
Author: Your Name <you@example.com>
Date:   Sun Jun 9 13:32:04 2024 +0200

    fix submodule

commit 5781a0cae4
Author: Your Name <you@example.com>
Date:   Sun Jun 9 13:04:02 2024 +0200

    basic test setup

commit 9da8d51f93
Author: Your Name <you@example.com>
Date:   Sun Jun 9 07:01:19 2024 +0200

    git http backend

commit 543beec2bf
Author: Your Name <you@example.com>
Date:   Sun Jun 9 06:55:53 2024 +0200

    bugs

commit db7ac5d068
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 6 19:59:32 2024 +0200

    lint

commit 3675958113
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 6 19:52:51 2024 +0200

    scope warning

commit 31bca3eb20
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 6 19:44:14 2024 +0200

    rm comments

commit ab840aac49
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 6 19:38:41 2024 +0200

    comment

commit 0e621ce35d
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 6 19:37:23 2024 +0200

    fix

commit 8b7d51d3b0
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 6 19:33:01 2024 +0200

    lsisteners

commit 3809386edb
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 6 19:27:33 2024 +0200

    rm comment

commit b76f022248
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 6 19:26:15 2024 +0200

    rm lint script

commit f9877e2c07
Merge: 8d5cda9d7 86034e401
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 6 19:25:25 2024 +0200

    Merge branch 'master' of https://github.com/ethereum/remix-project into git4refactor2

commit 8d5cda9d7e
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 6 19:23:45 2024 +0200

    lint

commit deea4edbd2
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 6 17:00:36 2024 +0200

    scope test

commit b5dcbc6ba3
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 6 16:39:15 2024 +0200

    openPanel

commit 62782ebe71
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 6 15:53:35 2024 +0200

    save creds

commit 8695d259bf
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Jun 6 15:15:12 2024 +0200

    fixes

commit fea8d9c5f6
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 12:45:23 2024 +0200

    vyper

commit 2e06f8534c
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 12:43:15 2024 +0200

    vyper

commit a01e309e6a
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 12:42:25 2024 +0200

    vyper

commit 8c1a134a56
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 12:16:58 2024 +0200

    lint

commit b277fab6e4
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 12:11:26 2024 +0200

    console

commit 38d518d551
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 12:08:12 2024 +0200

    fix user load

commit 2c9ecd01c0
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 12:02:46 2024 +0200

    vyper

commit f874259ceb
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 11:36:41 2024 +0200

    fix local plugin

commit 4ad97eed80
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 10:44:43 2024 +0200

    fix plugin test

commit 1a35705510
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 09:28:20 2024 +0200

    zoom test

commit 2324e82c6b
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 09:24:51 2024 +0200

    lint

commit 9c850f7bbe
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 09:24:18 2024 +0200

    clean up

commit d95bfe1797
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 08:55:56 2024 +0200

    fix checkout

commit 82f70f8a84
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 08:20:51 2024 +0200

    lint

commit 86ebcd0c1e
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 08:12:17 2024 +0200

    rm ratelimit

commit 597a559992
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 07:11:02 2024 +0200

    vyper fix

commit 30100b39ef
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 06:49:50 2024 +0200

    lint

commit 2dd6e7dec6
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 06:49:08 2024 +0200

    zoom test

commit 10b2a27ce5
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 06:36:07 2024 +0200

    hometab

commit c1435c41cd
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 06:27:20 2024 +0200

    fix editor mounted

commit e8b6a2fe92
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 05:22:45 2024 +0200

    lint

commit 3866568488
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 05:21:52 2024 +0200

    editor test

commit b93dea9325
Merge: 601a9f1a0 326585ffc
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Jun 5 05:15:57 2024 +0200

    Merge branch 'master' of https://github.com/ethereum/remix-project into git4refactor2

commit 601a9f1a08
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 4 14:02:07 2024 +0200

    fix editor

commit c790b92a0c
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 4 11:33:39 2024 +0200

    fix bugs

commit 067be4a40c
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 4 11:17:49 2024 +0200

    fix editors

commit a403c9f981
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 4 10:46:06 2024 +0200

    ifx bug

commit dff89e191d
Merge: bc9e9fd26 91aaf32ab
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Jun 4 08:18:56 2024 +0200

    Merge branch 'git4refactor2' of https://github.com/ethereum/remix-project into git4refactor2

commit 91aaf32ab8
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jun 3 09:29:23 2024 +0200

    lint

commit 5964cfd619
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jun 3 09:15:32 2024 +0200

    fix lint

commit 652b3b3ef7
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jun 3 09:14:16 2024 +0200

    delay activation

commit 200ab4d557
Merge: 9459e23d0 aedb13b7f
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jun 3 08:49:46 2024 +0200

    Merge branch 'master' of https://github.com/ethereum/remix-project into git4refactor2

commit 9459e23d0e
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Jun 3 08:48:04 2024 +0200

    add init method

commit a61d3304a1
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sun Jun 2 11:08:09 2024 +0200

    event duplication

commit 75642c450d
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu May 30 14:33:05 2024 +0200

    change url

commit 320f4420bd
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed May 22 08:24:00 2024 +0200

    add action types

commit 9b0389616e
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon May 13 08:15:51 2024 +0200

    fix app bug

commit 1d332e412c
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon May 13 08:09:22 2024 +0200

    start setup

commit 40b0b93558
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon May 13 07:45:49 2024 +0200

    add api

commit aec183366f
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon May 13 06:57:25 2024 +0200

    move api

commit 1b25d56827
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Mon May 13 06:29:47 2024 +0200

    cleanup

commit e7bf5d32a8
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Fri May 10 13:52:59 2024 +0200

    fix bug

commit 4341ce7094
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Fri May 10 13:23:10 2024 +0200

    fix bug

commit cbc2da066c
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Fri May 10 13:07:34 2024 +0200

    prefix bug

commit c0357ae03e
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Mon May 6 14:16:20 2024 +0200

    refactoring

commit 12c587ff57
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat May 4 07:34:18 2024 +0200

    remote refactor

commit 0a6c952726
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat May 4 06:22:51 2024 +0200

    faglobe

commit 74a872f0ac
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri May 3 08:47:50 2024 +0200

    fetching after push

commit 819202ba09
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri May 3 07:51:10 2024 +0200

    refresh

commit 53eee90070
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri May 3 07:25:03 2024 +0200

    fixes

commit 353b5670ad
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu May 2 21:54:03 2024 +0200

    listeners

commit 36ec094dd4
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu May 2 15:19:33 2024 +0200

    openremote

commit 9ade399399
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu May 2 13:21:05 2024 +0200

    ahead of

commit 0933d66896
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu May 2 08:34:54 2024 +0200

    git buttons

commit 86865ccccb
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu May 2 07:58:31 2024 +0200

    git json

commit 6cddc4a210
Merge: c03bb3553 b04745e97
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed May 1 06:23:52 2024 +0200

    Merge branch 'master' of https://github.com/ethereum/remix-project into git4

commit c03bb35533
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed May 1 06:15:42 2024 +0200

    remix-lib lint

commit c3db5384c9
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed May 1 06:15:21 2024 +0200

    lint remix-ui

commit 32d9abf089
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed May 1 06:08:14 2024 +0200

    lint ide

commit 7da0d585f3
Merge: 29cb47159 cc49cc54c
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed May 1 06:06:56 2024 +0200

    Merge branch 'git4' of https://github.com/ethereum/remix-project into git4

commit 29cb471590
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed May 1 06:06:47 2024 +0200

    sc base

commit cc49cc54cf
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Tue Apr 30 21:36:22 2024 +0200

    lmit script

commit 6a16d5423a
Author: bunsenstraat <filip.mertens@ethereum.org>
Date:   Tue Apr 30 21:35:37 2024 +0200

    lint config

commit 1e840f49ad
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Apr 30 08:16:53 2024 +0200

    fix sourcecontrol buttons

commit c46e181394
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Apr 30 07:17:58 2024 +0200

    bugfix

commit e1c1d94336
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Apr 29 11:19:16 2024 +0200

    change upstream object

commit 5b3df39c37
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Apr 29 09:48:08 2024 +0200

    rm lint target

commit 27e2c1d93d
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Apr 29 09:44:06 2024 +0200

    diff

commit f8a72e6043
Merge: d1544424b 15375e765
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sun Apr 28 07:46:41 2024 +0200

    Merge branch 'master' of https://github.com/ethereum/remix-project into git4

commit d1544424b4
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sun Apr 28 07:46:32 2024 +0200

    FM diff

commit d843e5c036
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat Apr 27 13:10:36 2024 +0200

    remote branches

commit aa7d58bad1
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Apr 24 08:03:22 2024 +0200

    add logger

commit e1fc893076
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Apr 23 16:33:41 2024 +0200

    add logger

commit 86c877989a
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Apr 23 12:00:04 2024 +0200

    rm gql

commit aee0919250
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Tue Apr 23 11:50:33 2024 +0200

    paged fetching data

commit c4dce2d804
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Apr 22 13:26:09 2024 +0200

    add device flow

commit 777f635f75
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Apr 22 11:56:19 2024 +0200

    add device code

commit be0be4c078
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Apr 22 09:06:13 2024 +0200

    add gui button

commit 9024f6e558
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Apr 22 06:46:46 2024 +0200

    loaders

commit 02e1d29084
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Apr 19 16:27:50 2024 +0200

    BranchDifferences

commit b3252a04a6
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Apr 19 11:34:09 2024 +0200

    setdefaultremote

commit ede22d886a
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Apr 19 10:48:29 2024 +0200

    branch diffs

commit 59ac71f571
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Apr 18 13:53:59 2024 +0200

    rename branchcommits

commit 5c0a0a610f
Merge: d791ff029 fabc441a3
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Apr 18 07:46:52 2024 +0200

    Merge branch 'master' of https://github.com/ethereum/remix-project into git4

commit d791ff0296
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sun Apr 7 09:22:30 2024 +0200

    ignore

commit 891f10aab8
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Apr 4 11:17:18 2024 +0200

    git provider

commit f298ba1e50
Merge: 8a304c09d c9513986f
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Apr 4 11:08:36 2024 +0200

    Merge branch 'master' of https://github.com/ethereum/remix-project into git4

commit 8a304c09d2
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Apr 3 16:56:22 2024 +0200

    pagecommits

commit 88c13ff961
Merge: 019ed19fc a7369679a
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat Mar 30 09:37:17 2024 +0100

    Merge branch 'master' of https://github.com/ethereum/remix-project into git4

commit 019ed19fc9
Merge: affc3e6ce 059251080
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Mar 29 07:59:00 2024 +0100

    Merge branch 'master' of https://github.com/ethereum/remix-project into git4

commit affc3e6ce8
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Feb 29 14:34:21 2024 +0100

    use octo

commit f4380cece5
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Wed Feb 28 07:48:56 2024 +0100

    types for gql

commit 66ef29f129
Author: bunsenstraat <filip@give.it>
Date:   Mon Feb 26 09:09:48 2024 +0100

    restore ts

commit 4ff34b68fd
Merge: 4fa723899 0a1b112c8
Author: bunsenstraat <filip@give.it>
Date:   Mon Feb 26 09:00:10 2024 +0100

    Merge branch 'master' of https://github.com/ethereum/remix-project into git4

commit 4fa7238999
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Feb 26 08:53:08 2024 +0100

    codegen

commit 0a5d2e2c84
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Feb 26 08:23:18 2024 +0100

    add gql

commit 6beeda8c6f
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Sat Feb 24 09:50:07 2024 +0100

    remote commits

commit 82caba939f
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Feb 23 10:40:59 2024 +0100

    refactor

commit 88cef7b6bf
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Feb 23 08:19:23 2024 +0100

    context

commit 63c15f03d6
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Thu Feb 15 09:00:53 2024 +0100

    translations

commit 933ae65fb8
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Mon Feb 12 13:34:11 2024 +0100

    font awesome

commit 51b2d20cb8
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Feb 9 09:57:40 2024 +0100

    add react select

commit 236b3095f4
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Feb 9 09:27:43 2024 +0100

    octokit dgitprovider

commit eded03fbcf
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Feb 9 08:46:13 2024 +0100

    add git to tsconfig paths

commit a769356ee1
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Feb 9 08:38:27 2024 +0100

    add appman hack

commit 097f528d29
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Feb 9 08:37:19 2024 +0100

    add dgit to app.js

commit afce79d418
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Feb 9 08:35:27 2024 +0100

    add git plugin

commit df103f9870
Author: filip mertens <filip.mertens@ethereum.org>
Date:   Fri Feb 9 08:33:06 2024 +0100

    add git components
pull/4877/head
bunsenstraat 8 months ago
parent 3964e4b3a6
commit 8aaa5ff374
  1. 4
      .gitignore
  2. 15
      apps/remix-ide-e2e/src/commands/addFile.ts
  3. 13
      apps/remix-ide-e2e/src/githttpbackend/package.json
  4. 48
      apps/remix-ide-e2e/src/githttpbackend/server.ts
  5. 7
      apps/remix-ide-e2e/src/githttpbackend/setup.sh
  6. 511
      apps/remix-ide-e2e/src/githttpbackend/yarn.lock
  7. 8
      apps/remix-ide-e2e/src/local-plugin/src/app/app.tsx
  8. 312
      apps/remix-ide-e2e/src/tests/dgit_github.test.ts
  9. 520
      apps/remix-ide-e2e/src/tests/dgit_local.test.ts
  10. 4
      apps/remix-ide-e2e/src/tests/editor.test.ts
  11. 2
      apps/remix-ide-e2e/src/tests/gist.test.ts
  12. 16
      apps/remix-ide-e2e/src/tests/plugin_api.ts
  13. 30
      apps/remix-ide-e2e/src/tests/remixd.test.ts
  14. 8
      apps/remix-ide-e2e/src/tests/vyper_api.test.ts
  15. 219
      apps/remix-ide-e2e/src/tests/workspace_git.test.ts
  16. 2
      apps/remix-ide-e2e/src/types/index.d.ts
  17. 8
      apps/remix-ide/src/app.js
  18. 35
      apps/remix-ide/src/app/editor/editor.js
  19. 635
      apps/remix-ide/src/app/files/dgitProvider.ts
  20. 40
      apps/remix-ide/src/app/files/fileManager.ts
  21. 11
      apps/remix-ide/src/app/panels/layout.ts
  22. 17
      apps/remix-ide/src/app/panels/tab-proxy.js
  23. 40
      apps/remix-ide/src/app/plugins/git.tsx
  24. 2
      apps/remix-ide/src/app/tabs/locales/en/filePanel.json
  25. 20
      apps/remix-ide/src/app/tabs/locales/en/git.json
  26. 1
      apps/remix-ide/src/app/udapp/run-tab.js
  27. 4
      apps/remix-ide/src/remixAppManager.js
  28. 2
      apps/remix-ide/src/remixEngine.js
  29. 14
      apps/remixdesktop/src/plugins/isoGitPlugin.ts
  30. 9
      apps/remixdesktop/src/types/index.ts
  31. 24
      apps/vyper/src/app/utils/remix-client.tsx
  32. 4
      apps/vyper/src/main.tsx
  33. 1
      libs/remix-api/src/index.ts
  34. 11
      libs/remix-api/src/lib/plugins/config-api.ts
  35. 10
      libs/remix-api/src/lib/plugins/fileSystem-api.ts
  36. 11
      libs/remix-api/src/lib/plugins/filedecorator-api.ts
  37. 31
      libs/remix-api/src/lib/plugins/notification-api.ts
  38. 11
      libs/remix-api/src/lib/plugins/settings-api.ts
  39. 19
      libs/remix-api/src/lib/remix-api.ts
  40. 82
      libs/remix-ui/editor/src/lib/actions/editor.ts
  41. 50
      libs/remix-ui/editor/src/lib/remix-ui-editor.tsx
  42. 70
      libs/remix-ui/git/src/components/branchHeader.tsx
  43. 147
      libs/remix-ui/git/src/components/buttons/commitmessage.tsx
  44. 24
      libs/remix-ui/git/src/components/buttons/gituibutton.tsx
  45. 90
      libs/remix-ui/git/src/components/buttons/sourceControlBase.tsx
  46. 98
      libs/remix-ui/git/src/components/buttons/sourcecontrolbuttons.tsx
  47. 12
      libs/remix-ui/git/src/components/disabled.tsx
  48. 50
      libs/remix-ui/git/src/components/github/branchselect.tsx
  49. 131
      libs/remix-ui/git/src/components/github/devicecode.tsx
  50. 87
      libs/remix-ui/git/src/components/github/repositoryselect.tsx
  51. 59
      libs/remix-ui/git/src/components/github/selectandclonerepositories.tsx
  52. 245
      libs/remix-ui/git/src/components/gitui.tsx
  53. 97
      libs/remix-ui/git/src/components/navigation/branchedetails.tsx
  54. 32
      libs/remix-ui/git/src/components/navigation/branches.tsx
  55. 29
      libs/remix-ui/git/src/components/navigation/clone.tsx
  56. 35
      libs/remix-ui/git/src/components/navigation/commands.tsx
  57. 37
      libs/remix-ui/git/src/components/navigation/commitdetails.tsx
  58. 63
      libs/remix-ui/git/src/components/navigation/commits.tsx
  59. 29
      libs/remix-ui/git/src/components/navigation/github.tsx
  60. 20
      libs/remix-ui/git/src/components/navigation/loaderindicator.tsx
  61. 87
      libs/remix-ui/git/src/components/navigation/log.tsx
  62. 49
      libs/remix-ui/git/src/components/navigation/menu/sourcecontrolmenu.tsx
  63. 32
      libs/remix-ui/git/src/components/navigation/remotes.tsx
  64. 68
      libs/remix-ui/git/src/components/navigation/remotesdetails.tsx
  65. 41
      libs/remix-ui/git/src/components/navigation/settings.tsx
  66. 42
      libs/remix-ui/git/src/components/navigation/sourcecontrol.tsx
  67. 52
      libs/remix-ui/git/src/components/navigation/sourcecontrolgroup.tsx
  68. 60
      libs/remix-ui/git/src/components/panels/branches.tsx
  69. 48
      libs/remix-ui/git/src/components/panels/branches/branchdifferencedetails.tsx
  70. 39
      libs/remix-ui/git/src/components/panels/branches/branchdifferences.tsx
  71. 86
      libs/remix-ui/git/src/components/panels/branches/localbranchdetails.tsx
  72. 111
      libs/remix-ui/git/src/components/panels/branches/remotebranchedetails.tsx
  73. 102
      libs/remix-ui/git/src/components/panels/clone.tsx
  74. 14
      libs/remix-ui/git/src/components/panels/commands.tsx
  75. 25
      libs/remix-ui/git/src/components/panels/commands/fetch.tsx
  76. 57
      libs/remix-ui/git/src/components/panels/commands/merge.tsx
  77. 196
      libs/remix-ui/git/src/components/panels/commands/pushpull.tsx
  78. 64
      libs/remix-ui/git/src/components/panels/commits.tsx
  79. 60
      libs/remix-ui/git/src/components/panels/commits/commitdetails.tsx
  80. 51
      libs/remix-ui/git/src/components/panels/commits/commitdetailsitem.tsx
  81. 70
      libs/remix-ui/git/src/components/panels/commits/commitsummary.tsx
  82. 90
      libs/remix-ui/git/src/components/panels/githubcredentials.tsx
  83. 29
      libs/remix-ui/git/src/components/panels/init.tsx
  84. 39
      libs/remix-ui/git/src/components/panels/log.tsx
  85. 53
      libs/remix-ui/git/src/components/panels/remotes.tsx
  86. 37
      libs/remix-ui/git/src/components/panels/remoteselect.tsx
  87. 82
      libs/remix-ui/git/src/components/panels/remotesimport.tsx
  88. 48
      libs/remix-ui/git/src/components/panels/settings.tsx
  89. 36
      libs/remix-ui/git/src/components/panels/setup.tsx
  90. 38
      libs/remix-ui/git/src/components/panels/sourcecontrol/sourcecontrolgroup.tsx
  91. 65
      libs/remix-ui/git/src/components/panels/sourcecontrol/sourcecontrolitem.tsx
  92. 76
      libs/remix-ui/git/src/components/panels/sourcecontrol/sourcontrolitembuttons.tsx
  93. 49
      libs/remix-ui/git/src/components/panels/sourcontrol.tsx
  94. 12
      libs/remix-ui/git/src/components/panels/tokenWarning.tsx
  95. 37
      libs/remix-ui/git/src/hooks/useLocalStorage.tsx
  96. 4
      libs/remix-ui/git/src/index.ts
  97. 56
      libs/remix-ui/git/src/lib/fileHelpers.ts
  98. 852
      libs/remix-ui/git/src/lib/gitactions.ts
  99. 201
      libs/remix-ui/git/src/lib/listeners.ts
  100. 100
      libs/remix-ui/git/src/lib/pluginActions.ts
  101. Some files were not shown because too many files have changed in this diff Show More

4
.gitignore vendored

@ -64,4 +64,6 @@ apps/remixdesktop/release/
apps/remix-ide/src/assets/list.json
apps/remix-ide/src/assets/esbuild.wasm
apps/remixdesktop/build*
logs
apps/remixdesktop/reports/
apps/remixdesktop/logs/
logs

@ -2,9 +2,11 @@ import { NightwatchBrowser, NightwatchContractContent } from 'nightwatch'
import EventEmitter from 'events'
class AddFile extends EventEmitter {
command(this: NightwatchBrowser, name: string, content: NightwatchContractContent): NightwatchBrowser {
command(this: NightwatchBrowser, name: string, content: NightwatchContractContent, readMeFile?:string): NightwatchBrowser {
if(!readMeFile)
readMeFile = 'README.txt'
this.api.perform((done) => {
addFile(this.api, name, content, () => {
addFile(this.api, name, content, readMeFile, () => {
done()
this.emit('complete')
})
@ -13,7 +15,8 @@ class AddFile extends EventEmitter {
}
}
function addFile(browser: NightwatchBrowser, name: string, content: NightwatchContractContent, done: VoidFunction) {
function addFile(browser: NightwatchBrowser, name: string, content: NightwatchContractContent, readMeFile:string, done: VoidFunction) {
const readmeSelector = `li[data-id="treeViewLitreeViewItem${readMeFile}"]`
browser
.isVisible({
selector: "//*[@data-id='sidePanelSwapitTitle' and contains(.,'File explorer')]",
@ -25,9 +28,9 @@ function addFile(browser: NightwatchBrowser, name: string, content: NightwatchCo
browser.clickLaunchIcon('filePanel')
}
})
.scrollInto('li[data-id="treeViewLitreeViewItemREADME.txt"]')
.waitForElementVisible('li[data-id="treeViewLitreeViewItemREADME.txt"]')
.click('li[data-id="treeViewLitreeViewItemREADME.txt"]').pause(1000) // focus on root directory
.scrollInto(readmeSelector)
.waitForElementVisible(readmeSelector)
.click(readmeSelector).pause(1000) // focus on root directory
.isVisible({
selector: `//*[@data-id="treeViewLitreeViewItem${name}"]`,
locateStrategy: 'xpath',

@ -0,0 +1,13 @@
{
"scripts": {
"start:server": "npx ts-node server.ts"
},
"dependencies": {
"body-parser": "^1.20.2",
"child_process": "^1.0.2",
"express": "^4.19.2",
"git-http-backend": "^1.1.2",
"path": "^0.12.7",
"zlib": "^1.0.5"
}
}

@ -0,0 +1,48 @@
import * as http from 'http';
import { spawn } from 'child_process';
import * as path from 'path';
let backend = require('git-http-backend');
import * as zlib from 'zlib';
const directory = process.argv[2];
if (!directory) {
console.error('Please provide a directory as a command line argument.');
process.exit(1);
}
const server = http.createServer((req, res) => {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
res.setHeader('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') {
// Handle preflight request
res.writeHead(204);
res.end();
return;
}
const repo = req.url?.split('/')[1];
const dir = path.join(directory, 'git', repo || '');
console.log(dir);
const reqStream = req.headers['content-encoding'] === 'gzip' ? req.pipe(zlib.createGunzip()) : req;
reqStream.pipe(backend(req.url || '', (err, service) => {
if (err) return res.end(err + '\n');
res.setHeader('content-type', service.type);
console.log(service.action, repo, service.fields);
const ps = spawn(service.cmd, [...service.args, dir]);
ps.stdout.pipe(service.createStream()).pipe(ps.stdin);
})).pipe(res);
});
server.listen(6868, () => {
console.log('Server is listening on port 6868');
});

@ -0,0 +1,7 @@
cd /tmp/
rm -rf git/bare.git
rm -rf git
mkdir -p git
cd git
git clone --bare https://github.com/ethereum/awesome-remix bare.git

@ -0,0 +1,511 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
dependencies:
mime-types "~2.1.34"
negotiator "0.6.3"
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
body-parser@1.20.2, body-parser@^1.20.2:
version "1.20.2"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
dependencies:
bytes "3.1.2"
content-type "~1.0.5"
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
http-errors "2.0.0"
iconv-lite "0.4.24"
on-finished "2.4.1"
qs "6.11.0"
raw-body "2.5.2"
type-is "~1.6.18"
unpipe "1.0.0"
bytes@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
call-bind@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==
dependencies:
es-define-property "^1.0.0"
es-errors "^1.3.0"
function-bind "^1.1.2"
get-intrinsic "^1.2.4"
set-function-length "^1.2.1"
child_process@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/child_process/-/child_process-1.0.2.tgz#b1f7e7fc73d25e7fd1d455adc94e143830182b5a"
integrity sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==
content-disposition@0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
dependencies:
safe-buffer "5.2.1"
content-type@~1.0.4, content-type@~1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
cookie@0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
define-data-property@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
dependencies:
es-define-property "^1.0.0"
es-errors "^1.3.0"
gopd "^1.0.1"
depd@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
destroy@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
es-define-property@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845"
integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==
dependencies:
get-intrinsic "^1.2.4"
es-errors@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
express@^4.19.2:
version "4.19.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
body-parser "1.20.2"
content-disposition "0.5.4"
content-type "~1.0.4"
cookie "0.6.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "2.0.0"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "1.2.0"
fresh "0.5.2"
http-errors "2.0.0"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "2.4.1"
parseurl "~1.3.3"
path-to-regexp "0.1.7"
proxy-addr "~2.0.7"
qs "6.11.0"
range-parser "~1.2.1"
safe-buffer "5.2.1"
send "0.18.0"
serve-static "1.15.0"
setprototypeof "1.2.0"
statuses "2.0.1"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
finalhandler@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "2.4.1"
parseurl "~1.3.3"
statuses "2.0.1"
unpipe "~1.0.0"
forwarded@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
has-proto "^1.0.1"
has-symbols "^1.0.3"
hasown "^2.0.0"
git-http-backend@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/git-http-backend/-/git-http-backend-1.1.2.tgz#fb6c7b261251df8c2bdf9dc30876a96ce323f545"
integrity sha512-Gx7n/kyCEXGFZlCGmbsEsyeyabLs8XWeb+E/6842up7p3PktQS2/8rlNfB6hCagnW0pJ13Tn8E3yhOkKS6ihdg==
dependencies:
git-side-band-message "~0.0.3"
inherits "~2.0.1"
git-side-band-message@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/git-side-band-message/-/git-side-band-message-0.0.3.tgz#b8a5348c2dcbf1949fd295c506014e26c3f26a46"
integrity sha512-4Rq4xm1+zqCkmuHxRbGdA5ActF7F4UfgK8uI0B7ZfSkByZfikRuF7mqHlvqmycvqos7jpXNkgsZK7DThLLHG3w==
gopd@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
dependencies:
get-intrinsic "^1.1.3"
has-property-descriptors@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
dependencies:
es-define-property "^1.0.0"
has-proto@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd"
integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==
has-symbols@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
hasown@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.2"
http-errors@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
dependencies:
depd "2.0.0"
inherits "2.0.4"
setprototypeof "1.2.0"
statuses "2.0.1"
toidentifier "1.0.1"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
inherits@2.0.4, inherits@~2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
ms@2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
negotiator@0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
object-inspect@^1.13.1:
version "1.13.1"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2"
integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
on-finished@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
dependencies:
ee-first "1.1.1"
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
path@^0.12.7:
version "0.12.7"
resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f"
integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==
dependencies:
process "^0.11.1"
util "^0.10.3"
process@^0.11.1:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
dependencies:
forwarded "0.2.0"
ipaddr.js "1.9.1"
qs@6.11.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
dependencies:
side-channel "^1.0.4"
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"
iconv-lite "0.4.24"
unpipe "1.0.0"
safe-buffer@5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
send@0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==
dependencies:
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "2.0.0"
mime "1.6.0"
ms "2.1.3"
on-finished "2.4.1"
range-parser "~1.2.1"
statuses "2.0.1"
serve-static@1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.18.0"
set-function-length@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
dependencies:
define-data-property "^1.1.4"
es-errors "^1.3.0"
function-bind "^1.1.2"
get-intrinsic "^1.2.4"
gopd "^1.0.1"
has-property-descriptors "^1.0.2"
setprototypeof@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
side-channel@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
dependencies:
call-bind "^1.0.7"
es-errors "^1.3.0"
get-intrinsic "^1.2.4"
object-inspect "^1.13.1"
statuses@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
toidentifier@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.24"
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
util@^0.10.3:
version "0.10.4"
resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==
dependencies:
inherits "2.0.3"
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
zlib@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/zlib/-/zlib-1.0.5.tgz#6e7c972fc371c645a6afb03ab14769def114fcc0"
integrity sha512-40fpE2II+Cd3k8HWTWONfeKE2jL+P42iWJ1zzps5W51qcTsOUKM5Q5m2PFb0CLxlmFAaUuUdJGc3OfZy947v0w==

@ -3,7 +3,6 @@ import {RemixPlugin} from './Client'
import {Logger} from './logger'
import {filePanelProfile} from '@remixproject/plugin-api'
import {filSystemProfile} from '@remixproject/plugin-api'
import {dGitProfile} from '@remixproject/plugin-api'
import {editorProfile} from '@remixproject/plugin-api'
import {settingsProfile} from '@remixproject/plugin-api'
import {networkProfile} from '@remixproject/plugin-api'
@ -12,8 +11,13 @@ import {compilerProfile} from '@remixproject/plugin-api'
import {contentImportProfile} from '@remixproject/plugin-api'
import {windowProfile} from '@remixproject/plugin-api'
import {pluginManagerProfile} from '@remixproject/plugin-api'
import {Profile} from '@remixproject/plugin-utils'
import {LibraryProfile, Profile} from '@remixproject/plugin-utils'
export const dGitProfile: LibraryProfile<any> = {
name: 'dgitApi',
methods: ['status', 'log', 'commit', 'add', 'branches'],
}
import './app.css'
const client = new RemixPlugin()

@ -0,0 +1,312 @@
import { ChildProcess, spawn } from "child_process"
import init from "../helpers/init"
import { Nightwatch, NightwatchBrowser } from "nightwatch"
module.exports = {
'@disabled': true,
before: function (browser, done) {
init(browser, done)
},
after: function (browser: NightwatchBrowser) {
browser.perform((done) => {
done()
})
},
'Update settings for git #group1 #group2': function (browser: NightwatchBrowser) {
browser.
clickLaunchIcon('dgit')
.waitForElementVisible('*[data-id="initgit-btn"]')
.click('*[data-id="initgit-btn"]')
.setValue('*[data-id="githubToken"]', process.env.dgit_token)
.setValue('*[data-id="gitubUsername"]', 'git')
.setValue('*[data-id="githubEmail"]', 'git@example.com')
.click('*[data-id="saveGitHubCredentials"]')
},
'check if the settings are loaded #group1 #group2': function (browser: NightwatchBrowser) {
browser.
click('*[data-id="github-panel"]')
.waitForElementVisible('*[data-id="connected-as-bunsenstraat"]')
.waitForElementVisible('*[data-id="connected-img-bunsenstraat"]')
.waitForElementVisible('*[data-id="connected-link-bunsenstraat"]')
},
'clone a repository #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="clone-panel"]')
.click({
selector: '//*[@data-id="clone-panel-content"]//*[@data-id="fetch-repositories"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="clone-panel-content"]//*[@id="repository-select"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="clone-panel-content"]//*[@id="repository-select"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="clone-panel-content"]//*[contains(text(), "awesome-remix")]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="clone-panel-content"]//*[contains(text(), "awesome-remix")]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="clone-panel-content"]//*[@id="branch-select"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="clone-panel-content"]//*[@id="branch-select"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="clone-panel-content"]//*[contains(text(), "master")]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="clone-panel-content"]//*[@data-id="clonebtn-ethereum/awesome-remix-master"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="clone-panel-content"]//*[@data-id="clonebtn-ethereum/awesome-remix-master"]',
locateStrategy: 'xpath'
})
},
'check if there is a README.md file #group1': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemREADME.md"]')
},
'check the commands panel #group1': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.click('*[data-id="commands-panel"]')
.waitForElementVisible({
selector: "//div[@id='commands-remote-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'master')]",
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: "//div[@id='commands-remote-origin-select']//div[contains(@class, 'singleValue') and contains(text(), 'origin')]",
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: "//div[@id='commands-local-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'master')]",
locateStrategy: 'xpath'
})
},
'check the remotes #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="remotes-panel"]')
.waitForElementVisible('*[data-id="remotes-panel-content"]')
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-detail-origin"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="branches-current-branch-master"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-sync-origin"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="branches-branch-links"]',
locateStrategy: 'xpath'
})
},
'check the commits of branch links #group1': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="branches-branch-links"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="branches-branch-links"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="commit-summary-linking fixed-"]',
locateStrategy: 'xpath'
})
},
'switch to branch links #group1': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="branches-branch-links"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="branches-toggle-branch-links"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="branches-toggle-current-branch-links"]',
locateStrategy: 'xpath'
})
},
'check the local branches #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="branches-panel"]')
.waitForElementVisible({
selector: '//*[@data-id="branches-panel-content"]//*[@data-id="branches-toggle-current-branch-links"]',
locateStrategy: 'xpath'
})
},
'check the local commits #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="commits-panel"]')
.pause(1000)
.waitForElementVisible({
selector: '//*[@data-id="commits-current-branch-links"]//*[@data-id="commit-summary-linking fixed-"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="commits-current-branch-links"]//*[@data-id="commit-summary-linking fixed-"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="commits-current-branch-links"]//*[@data-id="commit-change-modified-README.md"]',
locateStrategy: 'xpath'
})
},
'check the commands panel for links #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="commands-panel"]')
.waitForElementVisible({
selector: "//div[@id='commands-remote-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'links')]",
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: "//div[@id='commands-remote-origin-select']//div[contains(@class, 'singleValue') and contains(text(), 'origin')]",
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: "//div[@id='commands-local-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'links')]",
locateStrategy: 'xpath'
})
},
'add a remote #group2': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.click('*[data-id="remotes-panel"]')
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="fetch-repositories"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@id="repository-select"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[@id="repository-select"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[contains(text(), "awesome-remix")]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[contains(text(), "awesome-remix")]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-panel-remotename"]',
locateStrategy: 'xpath'
})
.setValue({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-panel-remotename"]',
locateStrategy: 'xpath'
}, 'newremote')
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-panel-addremote"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-panel-addremote"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-detail-newremote"]',
locateStrategy: 'xpath'
})
},
'check the commands panel for newremote #group2': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.click('*[data-id="commands-panel"]')
.waitForElementVisible({
selector: "//div[@id='commands-remote-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'main')]",
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: "//div[@id='commands-remote-origin-select']//div[contains(@class, 'singleValue') and contains(text(), 'newremote')]",
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: "//div[@id='commands-local-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'main')]",
locateStrategy: 'xpath'
})
.getAttribute({
selector: '//*[@data-id="sourcecontrol-pull"]',
locateStrategy: 'xpath'
}, 'disabled', (result) => {
if (result.value) {
browser.assert.fail('Button is disabled')
} else {
browser.assert.ok(true)
}
})
},
'remove the remove #group2': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.click('*[data-id="remotes-panel"]')
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-rm-newremote"]',
locateStrategy: 'xpath'
})
.pause(2000)
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-rm-newremote"]',
locateStrategy: 'xpath'
})
.pause(1000)
.waitForElementNotPresent({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-detail-newremote"]',
locateStrategy: 'xpath'
})
},
'check the commands panel for removed remote #group2': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.click('*[data-id="commands-panel"]')
.waitForElementVisible({
selector: "//div[@id='commands-remote-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'main')]",
locateStrategy: 'xpath'
})
.waitForElementNotPresent({
selector: "//div[@id='commands-remote-origin-select']//div[contains(@class, 'singleValue') and contains(text(), 'newremote')]",
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: "//div[@id='commands-local-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'main')]",
locateStrategy: 'xpath'
})
.getAttribute({
selector: '//*[@data-id="sourcecontrol-pull"]',
locateStrategy: 'xpath'
}, 'disabled', (result) => {
if (result.value) {
browser.assert.ok(true)
} else {
browser.assert.fail('Button is not disabled')
}
})
},
}

@ -0,0 +1,520 @@
import { ChildProcess, spawn } from "child_process"
import kill from 'tree-kill'
import init from "../helpers/init"
import { Nightwatch, NightwatchBrowser } from "nightwatch"
let gitserver: ChildProcess
/*
/ uses the git-http-backend package to create a git server ( if needed kill the server: kill -9 $(sudo lsof -t -i:6868) )
/ GROUP 1: file operations PUSH PULL COMMIT SYNC FETCH CLONE ADD
/ GROUP 2: branch operations CREATE & PUBLISH
/ GROUP 3: file operations rename delete
*/
module.exports = {
'@disabled': true,
before: function (browser, done) {
init(browser, done)
},
after: function (browser: NightwatchBrowser) {
browser.perform((done) => {
console.log('kill server', gitserver.pid)
kill(gitserver.pid)
done()
})
},
'run server #group1 #group2 #group3': function (browser: NightwatchBrowser) {
browser.perform(async (done) => {
gitserver = await spawnGitServer('/tmp/')
console.log('working directory', process.cwd())
done()
})
},
'Update settings for git #group1 #group2 #group3': function (browser: NightwatchBrowser) {
browser.
clickLaunchIcon('dgit')
.waitForElementVisible('*[data-id="initgit-btn"]')
.click('*[data-id="initgit-btn"]')
.setValue('*[data-id="gitubUsername"]', 'git')
.setValue('*[data-id="githubEmail"]', 'git@example.com')
.click('*[data-id="saveGitHubCredentials"]')
.modalFooterOKClick('github-credentials-error')
.pause(2000)
},
'clone a repo #group1 #group2 #group3': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="clone-panel"]')
.click('*[data-id="clone-panel"]')
.waitForElementVisible('*[data-id="clone-url"]')
.setValue('*[data-id="clone-url"]', 'http://localhost:6868/bare.git')
.waitForElementVisible('*[data-id="clone-btn"]')
.click('*[data-id="clone-btn"]')
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemREADME.md"]')
},
// GROUP 1
'check file added #group1 #group3': function (browser: NightwatchBrowser) {
browser.
addFile('test.txt', { content: 'hello world' }, 'README.md')
.clickLaunchIcon('dgit')
.click('*[data-id="sourcecontrol-panel"]')
.waitForElementVisible({
selector: "//*[@data-status='new-untracked' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.waitForElementVisible('*[data-id="addToGitChangestest.txt"]')
.pause(1000)
.click('*[data-id="addToGitChangestest.txt"]')
.waitForElementVisible({
selector: "//*[@data-status='added-staged' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.setValue('*[data-id="commitMessage"]', 'testcommit')
.click('*[data-id="commitButton"]')
},
'look at the commit #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="commits-panel"]')
.waitForElementPresent({
selector: '//*[@data-id="commit-summary-testcommit-ahead"]',
locateStrategy: 'xpath'
})
},
'sync the commit #group1': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.waitForElementVisible('*[data-id="sourcecontrol-panel"]')
.click('*[data-id="sourcecontrol-panel"]')
.waitForElementVisible('*[data-id="syncButton"]')
.click('*[data-id="syncButton"]')
.pause(2000)
.waitForElementVisible('*[data-id="commitButton"]')
.click('*[data-id="commits-panel"]')
.waitForElementPresent({
selector: '//*[@data-id="commit-summary-testcommit-"]',
locateStrategy: 'xpath'
})
},
'check the log #group1': async function (browser: NightwatchBrowser) {
const logs = await getGitLog('/tmp/git/bare.git')
console.log(logs)
browser.assert.ok(logs.includes('testcommit'))
},
'change a file #group1': function (browser: NightwatchBrowser) {
browser.
openFile('test.txt').
pause(1000).
setEditorValue('changes', null)
},
'stage changed file #group1': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.click('*[data-id="sourcecontrol-panel"]')
.waitForElementVisible({
selector: "//*[@data-status='modified-unstaged' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.waitForElementVisible('*[data-id="addToGitChangestest.txt"]')
.click('*[data-id="addToGitChangestest.txt"]')
.waitForElementVisible({
selector: "//*[@data-status='modified-staged' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.setValue('*[data-id="commitMessage"]', 'testcommit2')
.click('*[data-id="commitButton"]')
},
'push the commit #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="commands-panel"]')
.waitForElementVisible('*[data-id="sourcecontrol-push"]')
.click('*[data-id="sourcecontrol-push"]')
.pause(2000)
.click('*[data-id="commits-panel"]')
.waitForElementPresent({
selector: '//*[@data-id="commit-summary-testcommit2-"]',
locateStrategy: 'xpath'
}).pause(2000)
},
'check the log for testcommit2 #group1': async function (browser: NightwatchBrowser) {
const logs = await getGitLog('/tmp/git/bare.git')
console.log(logs)
browser.assert.ok(logs.includes('testcommit2'))
},
'clone locally and add a file and push #group1': async function (browser: NightwatchBrowser) {
await cloneOnServer('http://localhost:6868/bare.git', '/tmp/')
await onLocalGitRepoAddFile('/tmp/bare/', 'test2.txt')
await createCommitOnLocalServer('/tmp/bare/', 'testlocal')
await onLocalGitRepoPush('/tmp/bare/', 'master')
},
'run a git fetch #group1': function (browser: NightwatchBrowser) {
browser
.pause(2000)
.click('*[data-id="commands-panel"]')
.waitForElementVisible('*[data-id="sourcecontrol-fetch-branch"]')
.click('*[data-id="sourcecontrol-fetch-branch"]')
.pause(2000)
.click('*[data-id="commits-panel"]')
.click('*[data-id="commits-panel-behind"]')
.waitForElementPresent({
selector: '//*[@data-id="commit-summary-testlocal-"]',
locateStrategy: 'xpath'
})
},
'run pull from the header #group1': function (browser: NightwatchBrowser) {
browser.
click('*[data-id="sourcecontrol-button-pull"]')
.waitForElementNotPresent('*[data-id="commits-panel-behind"]')
},
'check if the file is added #group1': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest2.txt"]')
},
// group 3
'rename a file #group3': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.txt"]')
.click('*[data-id="treeViewLitreeViewItemtest.txt"]')
.renamePath('test.txt', 'test_rename', 'test_rename.txt')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest_rename.txt"]')
.pause(1000)
},
'stage renamed file #group3': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.waitForElementVisible({
selector: "//*[@data-status='deleted-unstaged' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.waitForElementVisible('*[data-id="addToGitChangestest.txt"]')
.waitForElementVisible({
selector: "//*[@data-status='new-untracked' and @data-file='/test_rename.txt']",
locateStrategy: 'xpath'
})
.click('*[data-id="sourcecontrol-add-all"]')
.pause(2000)
.waitForElementVisible({
selector: "//*[@data-status='deleted-staged' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: "//*[@data-status='added-staged' and @data-file='/test_rename.txt']",
locateStrategy: 'xpath'
})
},
'undo the rename #group3': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="unDoStagedtest.txt"]')
.pause(1000)
.waitForElementNotPresent({
selector: "//*[@data-file='/test.txt']",
locateStrategy: 'xpath'
})
},
'check if file is returned #group3': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.txt"]')
},
// GROUP 2
'create a branch #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.click('*[data-id="branches-panel"]')
.waitForElementVisible('*[data-id="newbranchname"]')
.setValue('*[data-id="newbranchname"]', 'testbranch')
.click('*[data-id="sourcecontrol-create-branch"]')
.waitForElementVisible('*[data-id="branches-current-branch-testbranch"]')
.pause(1000)
},
'check if the branch is in the filePanel #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.click('[data-id="workspaceGitBranchesDropdown"]')
.expect.element('[data-id="workspaceGit-testbranch"]').text.to.contain('✓ ')
},
'publish the branch #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.waitForElementVisible('*[data-id="sourcecontrol-panel"]')
.click('*[data-id="sourcecontrol-panel"]')
.pause(1000)
.click('*[data-id="publishBranchButton"]')
.pause(2000)
.waitForElementNotVisible('*[data-id="publishBranchButton"]')
},
'check if the branch is published #group2': async function (browser: NightwatchBrowser) {
const branches = await getBranches('/tmp/git/bare.git')
browser.assert.ok(branches.includes('testbranch'))
},
'add file to new branch #group2': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.addFile('test.txt', { content: 'hello world' }, 'README.md')
.clickLaunchIcon('dgit')
.waitForElementVisible({
selector: "//*[@data-status='new-untracked' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.waitForElementVisible('*[data-id="addToGitChangestest.txt"]')
.pause(1000)
.click('*[data-id="addToGitChangestest.txt"]')
.waitForElementVisible({
selector: "//*[@data-status='added-staged' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.setValue('*[data-id="commitMessage"]', 'testcommit')
.click('*[data-id="commitButton"]')
.pause(1000)
},
'check if the commit is ahead in the branches list #group2': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="branches-panel"]')
.click('*[data-id="branches-panel"]')
.waitForElementVisible('*[data-id="branches-current-branch-testbranch"]')
.click({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='branches-current-branch-testbranch']",
locateStrategy: 'xpath',
suppressNotFoundErrors: true
})
.click({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='commits-panel-ahead']",
locateStrategy: 'xpath',
suppressNotFoundErrors: true
})
.click({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='branchdifference-commits-testbranch-ahead']//*[@data-id='commit-summary-testcommit-ahead']",
locateStrategy: 'xpath',
})
.click({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='branchdifference-commits-testbranch-ahead']//*[@data-id='commit-change-added-test.txt']",
locateStrategy: 'xpath',
})
.click({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='local-branch-commits-testbranch']//*[@data-id='commit-summary-testcommit-ahead']",
locateStrategy: 'xpath',
})
.waitForElementVisible({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='local-branch-commits-testbranch']//*[@data-id='commit-change-added-test.txt']",
locateStrategy: 'xpath',
})
},
'switch back to master #group2': function (browser: NightwatchBrowser) {
browser
.click({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='branches-toggle-branch-master']",
locateStrategy: 'xpath',
})
.waitForElementVisible({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='branches-toggle-current-branch-master']",
locateStrategy: 'xpath',
})
},
'check if test file is gone #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.txt"]')
}
}
async function getBranches(path: string): Promise<string> {
return new Promise((resolve, reject) => {
const git = spawn('git', ['branch'], { cwd: path })
let branches = ''
git.stdout.on('data', function (data) {
console.log('stdout git branches', data.toString())
branches += data.toString()
})
git.stderr.on('data', function (data) {
console.log('stderr git branches', data.toString())
reject(data.toString())
})
git.on('close', function () {
resolve(branches)
})
})
}
async function getGitLog(path: string): Promise<string> {
return new Promise((resolve, reject) => {
const git = spawn('git', ['log'], { cwd: path })
let logs = ''
git.stdout.on('data', function (data) {
logs += data.toString()
})
git.stderr.on('err', function (data) {
reject(data.toString())
})
git.on('close', function () {
resolve(logs)
})
})
}
async function cloneOnServer(repo: string, path: string) {
console.log('cloning', repo, path)
return new Promise((resolve, reject) => {
const git = spawn('rm -rf bare && git', ['clone', repo], { cwd: path, shell: true, detached: true });
git.stdout.on('data', function (data) {
console.log('stdout data cloning', data.toString());
if (data.toString().includes('done')) {
resolve(git);
}
});
git.stderr.on('data', function (data) {
console.log('stderr data cloning', data.toString());
if (data.toString().includes('into')) {
setTimeout(() => {
resolve(git);
}, 5000)
}
});
git.on('error', (error) => {
reject(`Process error: ${error.message}`);
});
git.on('exit', (code, signal) => {
if (code !== 0) {
reject(`Process exited with code: ${code} and signal: ${signal}`);
}
});
});
}
async function onLocalGitRepoAddFile(path: string, file: string) {
console.log('adding file', file)
return new Promise((resolve, reject) => {
const git = spawn('touch', [file], { cwd: path });
git.stdout.on('data', function (data) {
console.log('stdout data adding file', data.toString());
if (data.toString().includes('done')) {
resolve(git);
}
});
git.stderr.on('data', function (data) {
console.error('stderr adding file', data.toString());
reject(data.toString());
});
git.on('error', (error) => {
reject(`Process error: ${error.message}`);
});
git.on('exit', (code, signal) => {
if (code !== 0) {
reject(`Process exited with code: ${code} and signal: ${signal}`);
} else {
resolve(git);
}
});
});
}
async function onLocalGitRepoPush(path: string, branch: string = 'master') {
console.log('pushing', path)
return new Promise((resolve, reject) => {
const git = spawn('git', ['push', 'origin', branch], { cwd: path, shell: true, detached: true });
git.stdout.on('data', function (data) {
console.log('stdout data pushing', data.toString());
if (data.toString().includes('done')) {
resolve(git);
}
});
git.stderr.on('data', function (data) {
console.error('stderr data pushing', data.toString());
if (data.toString().includes(branch)) {
resolve(git);
}
});
git.on('error', (error) => {
reject(`Process error: ${error.message}`);
});
git.on('exit', (code, signal) => {
if (code !== 0) {
reject(`Process exited with code: ${code} and signal: ${signal}`);
} else {
resolve(git);
}
});
});
}
async function createCommitOnLocalServer(path: string, message: string) {
console.log('committing', message, path)
return new Promise((resolve, reject) => {
const git = spawn('git add . && git', ['commit', '-m', message], { cwd: path, shell: true, detached: true });
git.stdout.on('data', function (data) {
console.log('data stdout committing', data.toString());
if (data.toString().includes(message)) {
setTimeout(() => {
resolve(git);
}, 1000)
}
});
git.stderr.on('data', function (data) {
console.error('data commiting', data.toString());
reject(data.toString());
});
git.on('error', (error) => {
console.error('error', error);
reject(`Process error: ${error.message}`);
});
git.on('exit', (code, signal) => {
if (code !== 0) {
console.error('exit', code, signal);
reject(`Process exited with code: ${code} and signal: ${signal}`);
} else {
resolve(git);
}
});
});
}
async function spawnGitServer(path: string): Promise<ChildProcess> {
console.log(process.cwd())
try {
const server = spawn('yarn && sh setup.sh && npx ts-node server.ts', [`${path}`], { cwd: process.cwd() + '/apps/remix-ide-e2e/src/githttpbackend/', shell: true, detached: true })
console.log('spawned', server.stdout.closed, server.stderr.closed)
return new Promise((resolve, reject) => {
server.stdout.on('data', function (data) {
console.log(data.toString())
if (
data.toString().includes('is listening')
|| data.toString().includes('address already in use')
) {
console.log('resolving')
resolve(server)
}
})
server.stderr.on('err', function (data) {
console.log(data.toString())
reject(data.toString())
})
})
} catch (e) {
console.log(e)
}
}

@ -20,13 +20,13 @@ module.exports = {
.checkElementStyle('.view-lines', 'font-size', '14px')
.click('*[data-id="tabProxyZoomIn"]')
.click('*[data-id="tabProxyZoomIn"]')
.checkElementStyle('.view-lines', 'font-size', '16.8px')
.checkElementStyle('.view-lines', 'font-size', '19.6px')
},
'Should zoom out editor #group1': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('#editorView')
.checkElementStyle('.view-lines', 'font-size', '16.8px')
.checkElementStyle('.view-lines', 'font-size', '19.6px')
.click('*[data-id="tabProxyZoomOut"]')
.click('*[data-id="tabProxyZoomOut"]')
.checkElementStyle('.view-lines', 'font-size', '14px')

@ -39,7 +39,7 @@ module.exports = {
// .perform((done) => { if (runtimeBrowser === 'chrome') { browser.openFile('gists') } done() })
.waitForElementVisible(`[data-id="treeViewLitreeViewItemREADME.txt"]`)
.openFile(`README.txt`)
//.openFile(`README.txt`)
// Remix publish to gist
/* .click('*[data-id="fileExplorerNewFilepublishToGist"]')
.pause(2000)

@ -10,7 +10,7 @@ declare global {
const localPluginData: Profile & LocationProfile & ExternalProfile = {
name: 'localPlugin',
displayName: 'Local Plugin',
canActivate: ['dGitProvider', 'flattener', 'solidityUnitTesting', 'udapp', 'hardhat-provider'],
canActivate: ['dgitApi', 'flattener', 'solidityUnitTesting', 'udapp', 'hardhat-provider'],
url: 'http://localhost:9999',
location: 'sidePanel'
}
@ -319,24 +319,24 @@ module.exports = {
// DGIT
'Should have changes on new workspace #group3': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:createWorkspace', null, null, 'dgit')
await clickAndCheckLog(browser, 'dGitProvider:status', [[".prettierrc.json",0,2,0], ["README.txt",0,2,0],["contracts/1_Storage.sol",0,2,0],["contracts/2_Owner.sol",0,2,0],["contracts/3_Ballot.sol",0,2,0],["scripts/deploy_with_ethers.ts",0,2,0],["scripts/deploy_with_web3.ts",0,2,0],["scripts/ethers-lib.ts",0,2,0],["scripts/web3-lib.ts",0,2,0],["tests/Ballot_test.sol",0,2,0],["tests/storage.test.js",0,2,0]], null, null)
await clickAndCheckLog(browser, 'dgitApi:status', [[".prettierrc.json",0,2,0], ["README.txt",0,2,0],["contracts/1_Storage.sol",0,2,0],["contracts/2_Owner.sol",0,2,0],["contracts/3_Ballot.sol",0,2,0],["scripts/deploy_with_ethers.ts",0,2,0],["scripts/deploy_with_web3.ts",0,2,0],["scripts/ethers-lib.ts",0,2,0],["scripts/web3-lib.ts",0,2,0],["tests/Ballot_test.sol",0,2,0],["tests/storage.test.js",0,2,0]], null, null)
},
'Should stage contract #group3': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'dGitProvider:add', null, null, {
await clickAndCheckLog(browser, 'dgitApi:add', null, null, {
filepath: 'contracts/1_Storage.sol'
})
await clickAndCheckLog(browser, 'dGitProvider:status', [[".prettierrc.json",0,2,0],["README.txt",0,2,0],["contracts/1_Storage.sol",0,2,2],["contracts/2_Owner.sol",0,2,0],["contracts/3_Ballot.sol",0,2,0],["scripts/deploy_with_ethers.ts",0,2,0],["scripts/deploy_with_web3.ts",0,2,0],["scripts/ethers-lib.ts",0,2,0],["scripts/web3-lib.ts",0,2,0],["tests/Ballot_test.sol",0,2,0],["tests/storage.test.js",0,2,0]], null, null)
await clickAndCheckLog(browser, 'dgitApi:status', [[".prettierrc.json",0,2,0],["README.txt",0,2,0],["contracts/1_Storage.sol",0,2,2],["contracts/2_Owner.sol",0,2,0],["contracts/3_Ballot.sol",0,2,0],["scripts/deploy_with_ethers.ts",0,2,0],["scripts/deploy_with_web3.ts",0,2,0],["scripts/ethers-lib.ts",0,2,0],["scripts/web3-lib.ts",0,2,0],["tests/Ballot_test.sol",0,2,0],["tests/storage.test.js",0,2,0]], null, null)
},
'Should commit changes #group3': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'dGitProvider:commit', null, null, { author: { name: 'Remix', email: 'Remix' }, message: 'commit-message' })
await clickAndCheckLog(browser, 'dGitProvider:log', 'commit-message', null, null)
await clickAndCheckLog(browser, 'dgitApi:commit', null, null, { author: { name: 'Remix', email: 'Remix' }, message: 'commit-message' })
await clickAndCheckLog(browser, 'dgitApi:log', 'commit-message', null, null)
},
'Should have git log #group3': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'dGitProvider:log', 'commit-message', null, null)
await clickAndCheckLog(browser, 'dgitApi:log', 'commit-message', null, null)
},
'Should have branches #group3': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'dGitProvider:branches', [{ name: 'main' }], null, null)
await clickAndCheckLog(browser, 'dgitApi:branches', [{ name: 'main' }], null, null)
},
// resolver
'Should resolve url #group4': async function (browser: NightwatchBrowser) {

@ -254,6 +254,28 @@ module.exports = {
})
},
'Should disable git when running remixd #group9': function (browser: NightwatchBrowser) {
browser.perform(async (done) => {
remixd = await spawnRemixd(join(process.cwd(), '/apps/remix-ide', '/contracts/hardhat'))
console.log('working directory', process.cwd())
connectRemixd(browser, done)
})
browser
.clickLaunchIcon('dgit')
.waitForElementVisible('*[data-id="disabled"]')
.pause(2000)
.clickLaunchIcon('filePanel')
.switchWorkspace('default_workspace')
.click({
selector:'[data-path="connectionAlert-modal-footer-ok-react"]',
suppressNotFoundErrors: true,
timeout: 2000
})
.pause(2000)
.clickLaunchIcon('dgit')
.waitForElementNotPresent('*[data-id="disabled"]')
}
}
@ -310,11 +332,11 @@ async function spawnRemixd(path: string): Promise<ChildProcess> {
const remixd = spawn('chmod +x dist/libs/remixd/src/bin/remixd.js && dist/libs/remixd/src/bin/remixd.js --remix-ide http://127.0.0.1:8080', [`-s ${path}`], { cwd: process.cwd(), shell: true, detached: true })
return new Promise((resolve, reject) => {
remixd.stdout.on('data', function (data) {
if(
data.toString().includes('is listening')
if (
data.toString().includes('is listening')
|| data.toString().includes('There is already a client running')
) {
) {
resolve(remixd)
}
})

@ -29,18 +29,14 @@ module.exports = {
.waitForElementVisible({
selector: "//*[@data-id='workspacesSelect' and contains(.,'snekmate')]",
locateStrategy: 'xpath',
timeout: 60000
timeout: 120000
})
.currentWorkspaceIs('snekmate')
.waitForElementVisible({
selector: "//*[@data-id='treeViewLitreeViewItemsrc' and contains(.,'src')]",
locateStrategy: 'xpath',
timeout: 60000
timeout: 1200000
})
.openFile('src')
.openFile('src/snekmate')
.openFile('src/snekmate/tokens')
.openFile('src/snekmate/tokens/ERC721.vy')
},
// 'Add vyper file to run tests #group1': function (browser: NightwatchBrowser) {
// browser.addFile('TestBallot.sol', sources[0]['TestBallot.sol'])

@ -40,6 +40,7 @@ module.exports = {
.click('[data-id="settingsTabSaveGistToken"]')
},
'Should create and initialize a GIT repository #group1': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
@ -59,6 +60,20 @@ module.exports = {
.waitForElementVisible('[data-id="workspaceGitPanel"]')
.waitForElementContainsText('[data-id="workspaceGitBranchesDropdown"]', 'main')
},
'check git for the commit #group1': function (browser: NightwatchBrowser) {
browser.
clickLaunchIcon('dgit')
.click('*[data-id="commits-panel"]')
.waitForElementPresent({
selector: '//*[@data-id="commits-current-branch-main"]//*[@data-id="commit-summary-Initial commit: remix template blank-"]',
locateStrategy: 'xpath'
})
.click('*[data-id="branches-panel"]')
.waitForElementPresent({
selector: '//*[@data-id="branches-panel-content"]//*[@data-id="branches-current-branch-main"]',
locateStrategy: 'xpath'
})
},
// CLONE REPOSITORY E2E START
@ -139,7 +154,7 @@ module.exports = {
.click('[data-id="fileSystem-modal-footer-ok-react"]')
.pause(5000)
.waitForElementVisible('[data-id="cloneGitRepositoryModalDialogModalBody-react"]')
.waitForElementContainsText('[data-id="cloneGitRepositoryModalDialogModalBody-react"]', 'An error occurred: Please check that you have the correct URL for the repo. If the repo is private, you need to add your github credentials (with the valid token permissions) in Settings plugin')
.waitForElementContainsText('[data-id="cloneGitRepositoryModalDialogModalBody-react"]', 'An error occurred: Please check that you have the correct URL for the repo. If the repo is private, you need to add your github credentials (with the valid token permissions) in the Git plugin')
.click('[data-id="cloneGitRepository-modal-footer-ok-react"]')
},
@ -311,66 +326,66 @@ module.exports = {
},
'When switching branches the submodules should disappear #group4': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('[data-id="workspaceGitBranchesDropdown"]')
.click('[data-id="workspaceGitBranchesDropdown"]')
.waitForElementVisible('[data-id="custom-dropdown-menu"]')
.waitForElementContainsText('[data-id="custom-dropdown-items"]', 'origin/empty')
.waitForElementPresent('[data-id="workspaceGit-origin/empty"]')
.click('[data-id="workspaceGit-origin/empty"]')
.waitForElementNotPresent('[data-id="treeViewDivtreeViewItemlibdeep"]')
.waitForElementNotPresent('[data-id="treeViewDivtreeViewItemtest-branch-submodule-recursive"]')
.waitForElementNotPresent('[data-id="treeViewDivtreeViewItemtest-branch-submodule-2"]')
.waitForElementVisible('[data-id="workspaceGitBranchesDropdown"]')
.click('[data-id="workspaceGitBranchesDropdown"]')
.waitForElementVisible('[data-id="custom-dropdown-menu"]')
.waitForElementContainsText('[data-id="custom-dropdown-items"]', 'origin/empty')
.waitForElementPresent('[data-id="workspaceGit-origin/empty"]')
.click('[data-id="workspaceGit-origin/empty"]')
.waitForElementNotPresent('[data-id="treeViewDivtreeViewItemlibdeep"]')
.waitForElementNotPresent('[data-id="treeViewDivtreeViewItemtest-branch-submodule-recursive"]')
.waitForElementNotPresent('[data-id="treeViewDivtreeViewItemtest-branch-submodule-2"]')
},
'When switching to main update the modules #group4': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('[data-id="workspaceGitBranchesDropdown"]')
.click('[data-id="workspaceGitBranchesDropdown"]')
.waitForElementVisible('[data-id="custom-dropdown-menu"]')
.waitForElementContainsText('[data-id="custom-dropdown-items"]', 'origin/main')
.waitForElementPresent('[data-id="workspaceGit-origin/main"]')
.click('[data-id="workspaceGit-origin/main"]')
.waitForElementVisible('[data-id="updatesubmodules"]')
.click('[data-id="updatesubmodules"]')
.waitForElementPresent('.fa-spinner')
.waitForElementVisible({
selector: '*[data-id="treeViewLitreeViewItem.git"]',
timeout: 240000
})
.pause(2000)
// check recursive submodule
.waitForElementVisible('[data-id="treeViewDivtreeViewItemtest-branch-submodule-recursive"]')
.click('[data-id="treeViewDivtreeViewItemtest-branch-submodule-recursive"]')
.click('[data-id="treeViewDivtreeViewItemtest-branch-submodule-recursive"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemtest-branch-submodule-recursive/test-branch-submodule-2"]')
.click('[data-id="treeViewDivtreeViewItemtest-branch-submodule-recursive/test-branch-submodule-2"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemtest-branch-submodule-recursive/test-branch-submodule-2/submodule2.ts"]')
// check test-branch-submodule-2 submodule
.waitForElementVisible('[data-id="treeViewDivtreeViewItemtest-branch-submodule-2"]')
.click('[data-id="treeViewDivtreeViewItemtest-branch-submodule-2"]')
.click('[data-id="treeViewDivtreeViewItemtest-branch-submodule-2"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemtest-branch-submodule-2/submodule2.ts"]')
// check libdeep submodule
.waitForElementVisible('[data-id="treeViewDivtreeViewItemlibdeep"]')
.click('[data-id="treeViewDivtreeViewItemlibdeep"]')
.click('[data-id="treeViewDivtreeViewItemlibdeep"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemlibdeep/test-branch-submodule-2"]')
.click('[data-id="treeViewDivtreeViewItemlibdeep/test-branch-submodule-2"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemlibdeep/test-branch-submodule-2/submodule2.ts"]')
// check libdeep2 submodule
.waitForElementVisible('[data-id="treeViewDivtreeViewItemlibdeep2"]')
.click('[data-id="treeViewDivtreeViewItemlibdeep2"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemlibdeep2/recursive"]')
.click('[data-id="treeViewDivtreeViewItemlibdeep2/recursive"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemlibdeep2/recursive/test-branch-submodule-2"]')
.click('[data-id="treeViewDivtreeViewItemlibdeep2/recursive/test-branch-submodule-2"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemlibdeep2/recursive/test-branch-submodule-2/submodule2.ts"]')
.waitForElementVisible('[data-id="workspaceGitBranchesDropdown"]')
.click('[data-id="workspaceGitBranchesDropdown"]')
.waitForElementVisible('[data-id="custom-dropdown-menu"]')
.waitForElementContainsText('[data-id="custom-dropdown-items"]', 'origin/main')
.waitForElementPresent('[data-id="workspaceGit-origin/main"]')
.click('[data-id="workspaceGit-origin/main"]')
.waitForElementVisible('[data-id="updatesubmodules"]')
.click('[data-id="updatesubmodules"]')
.waitForElementPresent('.fa-spinner')
.waitForElementVisible({
selector: '*[data-id="treeViewLitreeViewItem.git"]',
timeout: 240000
})
.pause(2000)
// check recursive submodule
.waitForElementVisible('[data-id="treeViewDivtreeViewItemtest-branch-submodule-recursive"]')
.click('[data-id="treeViewDivtreeViewItemtest-branch-submodule-recursive"]')
.click('[data-id="treeViewDivtreeViewItemtest-branch-submodule-recursive"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemtest-branch-submodule-recursive/test-branch-submodule-2"]')
.click('[data-id="treeViewDivtreeViewItemtest-branch-submodule-recursive/test-branch-submodule-2"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemtest-branch-submodule-recursive/test-branch-submodule-2/submodule2.ts"]')
// check test-branch-submodule-2 submodule
.waitForElementVisible('[data-id="treeViewDivtreeViewItemtest-branch-submodule-2"]')
.click('[data-id="treeViewDivtreeViewItemtest-branch-submodule-2"]')
.click('[data-id="treeViewDivtreeViewItemtest-branch-submodule-2"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemtest-branch-submodule-2/submodule2.ts"]')
// check libdeep submodule
.waitForElementVisible('[data-id="treeViewDivtreeViewItemlibdeep"]')
.click('[data-id="treeViewDivtreeViewItemlibdeep"]')
.click('[data-id="treeViewDivtreeViewItemlibdeep"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemlibdeep/test-branch-submodule-2"]')
.click('[data-id="treeViewDivtreeViewItemlibdeep/test-branch-submodule-2"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemlibdeep/test-branch-submodule-2/submodule2.ts"]')
// check libdeep2 submodule
.waitForElementVisible('[data-id="treeViewDivtreeViewItemlibdeep2"]')
.click('[data-id="treeViewDivtreeViewItemlibdeep2"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemlibdeep2/recursive"]')
.click('[data-id="treeViewDivtreeViewItemlibdeep2/recursive"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemlibdeep2/recursive/test-branch-submodule-2"]')
.click('[data-id="treeViewDivtreeViewItemlibdeep2/recursive/test-branch-submodule-2"]')
.waitForElementVisible('[data-id="treeViewDivtreeViewItemlibdeep2/recursive/test-branch-submodule-2/submodule2.ts"]')
},
// GIT SUBMODULES E2E ENDS
// GIT SUBMODULES E2E ENDS
// GIT WORKSPACE E2E STARTS
// GIT WORKSPACE E2E STARTS
'Should create a git workspace (uniswapV4Template) #group4': function (browser: NightwatchBrowser) {
'Should create a git workspace (uniswapV4Template) #group4': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="workspacesMenuDropdown"]')
.click('*[data-id="workspacecreate"]')
@ -388,8 +403,102 @@ module.exports = {
.getEditorValue((content) => {
browser.assert.ok(content.indexOf(`contract Counter is BaseHook {`) !== -1,
'Incorrect content')
}).pause()
},
'Should create Remix default workspace with files #group5': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.click('*[data-id="workspacesMenuDropdown"]')
.click('*[data-id="workspacecreate"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > button')
// eslint-disable-next-line dot-notation
.click('select[id="wstemplate"]')
.click('select[id="wstemplate"] option[value=ozerc20]')
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'new_workspace' })
.waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.waitForElementVisible('*[data-id="treeViewDivDraggableItemtests/MyToken_test.sol"]')
},
'Update settings for git #group5': function (browser: NightwatchBrowser) {
browser.
clickLaunchIcon('dgit')
.waitForElementVisible('*[data-id="initgit-btn"]')
.click('*[data-id="initgit-btn"]')
.setValue('*[data-id="gitubUsername"]', 'git')
.setValue('*[data-id="githubEmail"]', 'git@example.com')
.click('*[data-id="saveGitHubCredentials"]')
.modalFooterOKClick('github-credentials-error')
},
'check source controle panel #group5': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible({
selector: "//*[@data-status='new-untracked' and @data-file='/tests/MyToken_test.sol']",
locateStrategy: 'xpath'
})
},
'switch workspace and check git #group5': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.switchWorkspace('default_workspace')
},
'check source controle panel again #group5': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.clickLaunchIcon('dgit')
.waitForElementVisible({
selector: '*[data-id="initgit-btn"]',
suppressNotFoundErrors: true
})
.click({
selector:'*[data-id="initgit-btn"]',
suppressNotFoundErrors: true
})
.pause(2000)
.waitForElementVisible({
selector: "//*[@data-status='new-untracked' and @data-file='/tests/storage.test.js']",
locateStrategy: 'xpath'
})
},
'Should create a git workspace (uniswapV4Template) #group5': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.click('*[data-id="workspacesMenuDropdown"]')
.click('*[data-id="workspacecreate"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > button')
.click('select[id="wstemplate"]')
.click('select[id="wstemplate"] option[value=uniswapV4Template]')
.waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.pause(100)
.waitForElementVisible('*[data-id="treeViewLitreeViewItemsrc"]')
.openFile('src')
.openFile('src/Counter.sol')
.pause(1000)
.getEditorValue((content) => {
browser.assert.ok(content.indexOf(`contract Counter is BaseHook {`) !== -1,
'Incorrect content')
})
},
'check source controle panel for uniswap #group5': function (browser: NightwatchBrowser) {
browser
.pause(5000)
.clickLaunchIcon('dgit')
.click('*[data-id="remotes-panel"]')
.waitForElementVisible('*[data-id="remotes-panel-content"]')
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-detail-origin"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-detail-origin" and contains(.,"v4-template")]',
locateStrategy: 'xpath'
})
},
},
// GIT WORKSPACE E2E ENDS

@ -11,7 +11,7 @@ declare module 'nightwatch' {
scrollInto(target: string): NightwatchBrowser
testContracts(fileName: string, contractCode: NightwatchContractContent, compiledContractNames: string[]): NightwatchBrowser
setEditorValue(value: string, callback?: () => void): NightwatchBrowser
addFile(name: string, content: NightwatchContractContent): NightwatchBrowser
addFile(name: string, content: NightwatchContractContent, readMeFile?: string): NightwatchBrowser
verifyContracts(compiledContractNames: string[], opts?: {wait: number; version?: string; runs?: string}): NightwatchBrowser
selectAccount(account?: string): NightwatchBrowser
clickFunction(fnFullName: string, expectedInput?: NightwatchClickFunctionExpectedInput): NightwatchBrowser

@ -54,6 +54,8 @@ import { electronTemplates } from './app/plugins/electron/templatesPlugin'
import { xtermPlugin } from './app/plugins/electron/xtermPlugin'
import { ripgrepPlugin } from './app/plugins/electron/ripgrepPlugin'
import { compilerLoaderPlugin, compilerLoaderPluginDesktop } from './app/plugins/electron/compilerLoaderPlugin'
import { GitPlugin } from './app/plugins/git'
import {SolCoder} from './app/plugins/solcoderAI'
const isElectron = require('is-electron')
@ -217,6 +219,9 @@ class AppComponent {
//---- templates
const templates = new TemplatesPlugin()
//---- git
const git = new GitPlugin()
//---------------- Solidity UML Generator -------------------------
const solidityumlgen = new SolidityUmlGen(appManager)
@ -350,6 +355,7 @@ class AppComponent {
solidityScript,
templates,
solcoder,
git,
pluginStateLogger
])
@ -490,7 +496,7 @@ class AppComponent {
])
await this.appManager.activatePlugin(['settings'])
await this.appManager.activatePlugin(['walkthrough', 'storage', 'search', 'compileAndRun', 'recorder'])
await this.appManager.activatePlugin(['walkthrough', 'storage', 'search', 'compileAndRun', 'recorder', 'dgit'])
await this.appManager.activatePlugin(['solidity-script', 'remix-templates'])
if (isElectron()){

@ -5,6 +5,7 @@ import { EditorUI } from '@remix-ui/editor' // eslint-disable-line
import { Plugin } from '@remixproject/engine'
import * as packageJson from '../../../../../package.json'
import { PluginViewWrapper } from '@remix-ui/helper'
import { commitChange } from '@remix-ui/git'
const EventManager = require('../../lib/events')
@ -13,7 +14,7 @@ const profile = {
name: 'editor',
description: 'service - editor',
version: packageJson.version,
methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addLineText', 'discardLineTexts', 'addAnnotation', 'gotoLine', 'revealRange', 'getCursorPosition', 'open', 'addModel','addErrorMarker', 'clearErrorMarkers', 'getText', 'getPositionAt'],
methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addLineText', 'discardLineTexts', 'addAnnotation', 'gotoLine', 'revealRange', 'getCursorPosition', 'open', 'addModel','addErrorMarker', 'clearErrorMarkers', 'getText', 'getPositionAt', 'openReadOnly'],
}
class Editor extends Plugin {
@ -63,7 +64,8 @@ class Editor extends Plugin {
onBreakPointAdded: (file, line) => this.triggerEvent('breakpointAdded', [file, line]),
onBreakPointCleared: (file, line) => this.triggerEvent('breakpointCleared', [file, line]),
onDidChangeContent: (file) => this._onChange(file),
onEditorMounted: () => this.triggerEvent('editorMounted', [])
onEditorMounted: () => this.triggerEvent('editorMounted', []),
onDiffEditorMounted: () => this.triggerEvent('diffEditorMounted', [])
}
// to be implemented by the react component
@ -81,8 +83,10 @@ class Editor extends Plugin {
editorAPI={state.api}
themeType={state.currentThemeType}
currentFile={state.currentFile}
currentDiffFile={state.currentDiffFile}
events={state.events}
plugin={state.plugin}
isDiff={state.isDiff}
/>
}
@ -111,6 +115,8 @@ class Editor extends Plugin {
api: this.api,
currentThemeType: this.currentThemeType,
currentFile: this.currentFile,
currentDiffFile: this.currentDiffFile,
isDiff: this.isDiff,
events: this.events,
plugin: this
})
@ -183,9 +189,10 @@ class Editor extends Plugin {
}
_switchSession (path) {
if (path === this.currentFile) return
this.triggerEvent('sessionSwitched', [])
this.currentFile = path
if (path !== this.currentFile) {
this.triggerEvent('sessionSwitched', [])
this.currentFile = path
}
this.renderComponent()
}
@ -241,10 +248,10 @@ class Editor extends Plugin {
* @param {string} content Content of the file to open
* @param {string} mode Mode for this file [Default is `text`]
*/
async _createSession (path, content, mode) {
async _createSession (path, content, mode, readOnly) {
if (!this.activated) return
this.emit('addModel', content, mode, path, this.readOnlySessions[path])
this.emit('addModel', content, mode, path, readOnly || this.readOnlySessions[path])
return {
path,
language: mode,
@ -314,6 +321,7 @@ class Editor extends Plugin {
- URL prepended with "browser"
- URL not prepended with the file explorer. We assume (as it is in the whole app, that this is a "browser" URL
*/
this.isDiff = false
if (!this.sessions[path]) {
this.readOnlySessions[path] = false
const session = await this._createSession(path, content, this._getMode(path))
@ -335,9 +343,22 @@ class Editor extends Plugin {
const session = await this._createSession(path, content, this._getMode(path))
this.sessions[path] = session
}
this.isDiff = false
this._switchSession(path)
}
async openDiff(change) {
console.log('openDiff', change)
const hashedPathModified = change.readonly ? change.path + change.hashModified : change.path
const hashedPathOriginal = change.path + change.hashOriginal
const session = await this._createSession(hashedPathModified, change.modified, this._getMode(change.path), change.readonly)
await this._createSession(hashedPathOriginal, change.original, this._getMode(change.path), change.readonly)
this.sessions[hashedPathModified] = session
this.currentDiffFile = hashedPathOriginal
this.isDiff = true
this._switchSession(hashedPathModified)
}
/**
* Content of the current session
* @return {String} content of the file referenced by @arg path

File diff suppressed because it is too large Load Diff

@ -8,6 +8,7 @@ import { Registry } from '@remix-project/remix-lib'
import { fileChangedToastMsg, recursivePasteToastMsg, storageFullMessage } from '@remix-ui/helper'
import helper from '../../lib/helper.js'
import { RemixAppManager } from '../../remixAppManager'
import { commitChange } from '@remix-ui/git'
/*
attach to files event (removed renamed)
@ -24,7 +25,7 @@ const profile = {
methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'writeMultipleFiles', 'writeFileNoRewrite',
'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'dirList', 'fileList', 'remove', 'getCurrentFile', 'getFile',
'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath',
'saveCurrentFile', 'setBatchFiles', 'isGitRepo', 'isFile', 'isDirectory', 'hasGitSubmodule', 'copyFolderToJson'
'saveCurrentFile', 'setBatchFiles', 'isGitRepo', 'isFile', 'isDirectory', 'hasGitSubmodule', 'copyFolderToJson', 'diff'
],
kind: 'file-system'
}
@ -702,6 +703,37 @@ class FileManager extends Plugin {
this.emit('noFileSelected')
}
async diff(change: commitChange) {
await this.saveCurrentFile()
this._deps.config.set('currentFile', '')
// TODO: Only keep `this.emit` (issue#2210)
this.emit('noFileSelected')
if (!change.readonly){
let file = this.normalize(change.path)
const resolved = this.getPathFromUrl(file)
file = resolved.file
this._deps.config.set('currentFile', file)
this.openedFiles[file] = file
}
await this.editor.openDiff(change)
this.emit('openDiff', change)
}
async closeDiff(change: commitChange) {
if (!change.readonly){
const file = this.normalize(change.path)
delete this.openedFiles[file]
if (!Object.keys(this.openedFiles).length) {
this._deps.config.set('currentFile', '')
// TODO: Only keep `this.emit` (issue#2210)
this.emit('noFileSelected')
}
}
this.emit('closeDiff', change)
}
async openFile(file?: string) {
if (!file) {
this.emit('noFileSelected')
@ -710,16 +742,17 @@ class FileManager extends Plugin {
const resolved = this.getPathFromUrl(file)
file = resolved.file
await this.saveCurrentFile()
if (this.currentFile() === file) return
// we always open the file in the editor, even if it's the same as the current one if the editor is in diff mode
if (this.currentFile() === file && !this.editor.isDiff) return
const provider = resolved.provider
this._deps.config.set('currentFile', file)
this.openedFiles[file] = file
let content = ''
try {
content = await provider.get(file)
} catch (error) {
console.log(error)
throw error
@ -736,6 +769,7 @@ class FileManager extends Plugin {
} else {
await this.editor.open(file, content)
}
// TODO: Only keep `this.emit` (issue#2210)
this.emit('currentFileChanged', file)
return true
}

@ -43,6 +43,11 @@ export class Layout extends Plugin {
this.panels.main.active = false
this.event.emit('change', null)
})
this.on('fileManager', 'openDiff', () => {
this.panels.editor.active = true
this.panels.main.active = false
this.event.emit('change', null)
})
this.on('tabs', 'openFile', () => {
this.panels.editor.active = true
this.panels.main.active = false
@ -59,6 +64,12 @@ export class Layout extends Plugin {
this.panels.main.active = false
this.event.emit('change', null)
})
this.on('tabs', 'openDiff', () => {
console.log('openDiff')
this.panels.editor.active = true
this.panels.main.active = false
this.event.emit('change', null)
})
this.on('manager', 'activate', (profile: Profile) => {
switch (profile.name) {
case 'filePanel':

@ -146,6 +146,23 @@ export class TabProxy extends Plugin {
}
})
this.on('fileManager', 'openDiff', (commit) => {
const hash = commit.hashModified? commit.hashModified.substring(0,6): 'Working Tree'
const name = `${commit.path} (${hash})`
this.addTab(name, name, async () => {
await this.fileManager.diff(commit)
this.event.emit('openDiff', commit)
this.emit('openDiff', commit)
},
async () => {
this.removeTab(name)
await this.fileManager.closeDiff(commit)
this.event.emit('closeDiff', commit)
this.emit('closeDiff', commit)
})
this.tabsApi.activateTab(name)
})
this.on('manager', 'pluginActivated', ({ name, location, displayName, icon, description }) => {
if (location === 'mainPanel') {
this.addTab(

@ -0,0 +1,40 @@
'use strict'
import { ViewPlugin } from '@remixproject/engine-web';
import React from 'react' // eslint-disable-line
import { gitState, GitUI } from '@remix-ui/git';
import * as packageJson from '../../../../../package.json'
const profile = {
name: 'dgit',
desciption: 'Git plugin for Remix',
methods: ['pull', 'track', 'diff', 'clone', 'open'],
events: [''],
version: packageJson.version,
maintainedBy: 'Remix',
permission: true,
description: 'Use this plugin to interact with your git repositories',
location: 'sidePanel',
icon: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIxLjAwNyA4LjIyMTY4QzIxLjAxMDUgNy41Mjc5MiAyMC44MjA3IDYuODQ2ODkgMjAuNDU5MSA2LjI1NDg1QzIwLjA5NzQgNS42NjI4MSAxOS41NzggNS4xODMxNSAxOC45NTkyIDQuODY5NTdDMTguMzQwMyA0LjU1NiAxNy42NDYzIDQuNDIwOTEgMTYuOTU1MSA0LjQ3OTQxQzE2LjI2MzcgNC41Mzc5MyAxNS42MDI1IDQuNzg3NzMgMTUuMDQ1IDUuMjAwODVDMTQuNDg3NyA1LjYxMzk3IDE0LjA1NjMgNi4xNzQwOSAxMy43OTkzIDYuODE4NUMxMy41NDI0IDcuNDYyOSAxMy40Njk3IDguMTY2MTMgMTMuNTg5OCA4Ljg0OTQ0QzEzLjcwOTkgOS41MzI3NCAxNC4wMTc3IDEwLjE2OTIgMTQuNDc4OSAxMC42ODc0QzE0Ljk0MDIgMTEuMjA1NiAxNS41MzY3IDExLjU4NTIgMTYuMjAxNSAxMS43ODM2QzE1Ljk1NiAxMi4yODI0IDE1LjU3NjMgMTIuNzAzIDE1LjEwNDkgMTIuOTk3OUMxNC42MzM2IDEzLjI5MjkgMTQuMDg5NCAxMy40NTA1IDEzLjUzMzQgMTMuNDUzMkgxMC41NDRDOS40MzcyNiAxMy40NTcxIDguMzcxNjMgMTMuODcyNyA3LjU1NDUxIDE0LjYxOTFWNy4zOTgwOUM4LjQ2MTg0IDcuMjEyODggOS4yNjgwOCA2LjY5NzM3IDkuODE2OTIgNS45NTE1MUMxMC4zNjU4IDUuMjA1NjUgMTAuNjE4MSA0LjI4MjU2IDEwLjUyNSAzLjM2MTIxQzEwLjQzMTkgMi40Mzk4NyAxMC4wMDAxIDEuNTg1OSA5LjMxMzE2IDAuOTY0ODczQzguNjI2MjQgMC4zNDM4NDUgNy43MzMxOSAwIDYuODA3MTYgMEM1Ljg4MTEyIDAgNC45ODgwNyAwLjM0Mzg0NSA0LjMwMTE0IDAuOTY0ODczQzMuNjE0MjIgMS41ODU5IDMuMTgyMzYgMi40Mzk4NyAzLjA4OTI4IDMuMzYxMjFDMi45OTYyIDQuMjgyNTYgMy4yNDg1NSA1LjIwNTY1IDMuNzk3MzkgNS45NTE1MUM0LjM0NjIzIDYuNjk3MzcgNS4xNTI0NyA3LjIxMjg4IDYuMDU5OCA3LjM5ODA5VjE2LjUxNTlDNS4xNTQxOCAxNi42ODkxIDQuMzQzMjMgMTcuMTg3NyAzLjc3OTkzIDE3LjkxNzZDMy4yMTY2MyAxOC42NDc2IDIuOTM5OTIgMTkuNTU4NSAzLjAwMTk3IDIwLjQ3ODVDMy4wNjQwMyAyMS4zOTg0IDMuNDYwNTcgMjIuMjYzOSA0LjExNjggMjIuOTExNUM0Ljc3MzAzIDIzLjU1OTIgNS42NDM2IDIzLjk0NDQgNi41NjQyNyAyMy45OTQ0QzcuNDg0OTYgMjQuMDQ0NSA4LjM5MjExIDIzLjc1NTggOS4xMTQ2NCAyMy4xODNDOS44MzcxOCAyMi42MTAyIDEwLjMyNTEgMjEuNzkyOCAxMC40ODY1IDIwLjg4NUMxMC42NDc4IDE5Ljk3NzEgMTAuNDcxNCAxOS4wNDE3IDkuOTkwNDggMTguMjU1QzkuNTA5NTcgMTcuNDY4MyA4Ljc1NzQxIDE2Ljg4NDggNy44NzU4OCAxNi42MTQ1QzguMTIxNzYgMTYuMTE2MiA4LjUwMTY3IDE1LjY5NjMgOC45NzI5NiAxNS40MDE5QzkuNDQ0MjYgMTUuMTA3NCA5Ljk4ODI3IDE0Ljk1MDMgMTAuNTQ0IDE0Ljk0NzlIMTMuNTMzNEMxNC40NjYxIDE0Ljk0MzYgMTUuMzc0MiAxNC42NDg2IDE2LjEzMTMgMTQuMTAzOUMxNi44ODg0IDEzLjU1OTIgMTcuNDU2OCAxMi43OTIgMTcuNzU3NSAxMS45MDkxQzE4LjY1MzQgMTEuNzkxNCAxOS40NzYzIDExLjM1MjggMjAuMDczOCAxMC42NzQ4QzIwLjY3MTMgOS45OTY4IDIxLjAwMjggOS4xMjUzMyAyMS4wMDcgOC4yMjE2OFpNNC41NjUwOCAzLjczNzUyQzQuNTY1MDggMy4yOTQwOCA0LjY5NjU3IDIuODYwNiA0Ljk0MjkzIDIuNDkxOUM1LjE4OTMgMi4xMjMxOSA1LjUzOTQ3IDEuODM1ODEgNS45NDkxNSAxLjY2NjExQzYuMzU4ODQgMS40OTY0MiA2LjgwOTY1IDEuNDUyMDIgNy4yNDQ1NiAxLjUzODU0QzcuNjc5NDggMS42MjUwNCA4LjA3ODk4IDEuODM4NTcgOC4zOTI1NCAyLjE1MjE0QzguNzA2MTEgMi40NjU3IDguOTE5NjQgMi44NjUyIDkuMDA2MTUgMy4zMDAxMkM5LjA5MjY2IDMuNzM1MDQgOS4wNDgyNyA0LjE4NTg1IDguODc4NTcgNC41OTU1M0M4LjcwODg3IDUuMDA1MjEgOC40MjE0OSA1LjM1NTM5IDguMDUyNzggNS42MDE3NUM3LjY4NDA4IDUuODQ4MTEgNy4yNTA2IDUuOTc5NiA2LjgwNzE2IDUuOTc5NkM2LjIxMjUyIDUuOTc5NiA1LjY0MjI0IDUuNzQzMzkgNS4yMjE3NyA1LjMyMjkxQzQuODAxMjkgNC45MDI0NSA0LjU2NTA4IDQuMzMyMTYgNC41NjUwOCAzLjczNzUyWk05LjA0OTIzIDIwLjE3OTRDOS4wNDkyMyAyMC42MjI5IDguOTE3NzQgMjEuMDU2MyA4LjY3MTM4IDIxLjQyNUM4LjQyNTAxIDIxLjc5MzcgOC4wNzQ4NSAyMi4wODExIDcuNjY1MTYgMjIuMjUwOEM3LjI1NTQ3IDIyLjQyMDUgNi44MDQ2NiAyMi40NjQ5IDYuMzY5NzUgMjIuMzc4NEM1LjkzNDgzIDIyLjI5MiA1LjUzNTMzIDIyLjA3ODQgNS4yMjE3NyAyMS43NjQ4QzQuOTA4MjEgMjEuNDUxMiA0LjY5NDY3IDIxLjA1MTcgNC42MDgxNiAyMC42MTY5QzQuNTIxNjUgMjAuMTgxOSA0LjU2NjA1IDE5LjczMTEgNC43MzU3NSAxOS4zMjE0QzQuOTA1NDUgMTguOTExNyA1LjE5MjgyIDE4LjU2MTUgNS41NjE1MyAxOC4zMTUyQzUuOTMwMjMgMTguMDY4OSA2LjM2MzcxIDE3LjkzNzMgNi44MDcxNiAxNy45MzczQzcuNDAxNzkgMTcuOTM3MyA3Ljk3MjA3IDE4LjE3MzYgOC4zOTI1NCAxOC41OTRDOC44MTMwMiAxOS4wMTQ1IDkuMDQ5MjMgMTkuNTg0OCA5LjA0OTIzIDIwLjE3OTRaTTE3LjI3MDIgMTAuNDYzOEMxNi44MjY3IDEwLjQ2MzggMTYuMzkzMyAxMC4zMzIyIDE2LjAyNDYgMTAuMDg1OUMxNS42NTU5IDkuODM5NTQgMTUuMzY4NSA5LjQ4OTM3IDE1LjE5ODggOS4wNzk2OUMxNS4wMjkxIDguNjcgMTQuOTg0NyA4LjIxOTIgMTUuMDcxMiA3Ljc4NDI3QzE1LjE1NzYgNy4zNDkzNSAxNS4zNzEyIDYuOTQ5ODUgMTUuNjg0OCA2LjYzNjI5QzE1Ljk5ODQgNi4zMjI3MyAxNi4zOTc5IDYuMTA5MTkgMTYuODMyNyA2LjAyMjY4QzE3LjI2NzcgNS45MzYxNyAxNy43MTg1IDUuOTgwNTggMTguMTI4MSA2LjE1MDI3QzE4LjUzNzkgNi4zMTk5NyAxOC44ODgxIDYuNjA3MzQgMTkuMTM0NCA2Ljk3NjA1QzE5LjM4MDcgNy4zNDQ3NiAxOS41MTIzIDcuNzc4MjMgMTkuNTEyMyA4LjIyMTY4QzE5LjUxMjMgOC44MTYzMiAxOS4yNzYgOS4zODY2IDE4Ljg1NTYgOS44MDcwNkMxOC40MzUxIDEwLjIyNzUgMTcuODY0OCAxMC40NjM4IDE3LjI3MDIgMTAuNDYzOFoiIGZpbGw9IiM0MjQyNDIiLz4KPC9zdmc+Cg=="
}
export class GitPlugin extends ViewPlugin {
constructor() {
super(profile)
}
onDeactivation(): void {
this.call('fileDecorator', 'clearFileDecorators')
this.call('manager', 'activatePlugin', 'dgitApi')
}
async open(panel:string) {
this.emit('openPanel', panel)
}
render() {
return <div id='gitTab'><GitUI plugin={this} /></div>
}
}

@ -78,7 +78,7 @@
"filePanel.checkoutGitBranch": "Checkout Git Branch",
"filePanel.findOrCreateABranch": "Find or create a branch.",
"filePanel.initGitRepositoryLabel": "Initialize workspace as a new git repository",
"filePanel.initGitRepositoryWarning": "To use Git features, add username and email to the Github section of the Settings panel.",
"filePanel.initGitRepositoryWarning": "To use Git features, add username and email to the Github section of the Git plugin.",
"filePanel.workspaceName": "Workspace name",
"filePanel.customizeTemplate": "Customize template",
"filePanel.features": "Features",

@ -0,0 +1,20 @@
{
"git.push": "push",
"git.pull": "pull",
"git.commit": "commit",
"git.sync": "sync",
"git.syncchanges": "sync changes",
"git.publish": "publish",
"git.ignore": "ignore",
"git.createBranch": "create branch",
"git.deleteBranch": "delete branch",
"git.mergeBranch": "merge branch",
"git.rebaseBranch": "rebase branch",
"git.checkout": "checkout",
"git.fetch": "fetch",
"git.refresh": "refresh",
"git.unstageall": "unstage all",
"git.stageall": "stage all",
"git.noremote": "this repo has no remotes",
"git.init": "Initialize repository"
}

@ -161,7 +161,6 @@ export class RunTab extends ViewPlugin {
await addProvider(position, name, displayName, true, false, false)
}
const registerInjectedProvider = async (event) => {
console.log('registerInjectedProvider', event)
const name = 'injected-' + event.detail.info.name
const displayName = 'Injected Provider - ' + event.detail.info.name
await this.engine.register([new InjectedProviderDefault(event.detail.provider, name)])

@ -35,7 +35,7 @@ let requiredModules = [ // services + layout views + system views
'pluginManager',
'tabs',
'udapp',
'dGitProvider',
'dgitApi',
'solidity',
'solidity-logic',
'gistHandler',
@ -77,6 +77,7 @@ let requiredModules = [ // services + layout views + system views
'doc-gen',
'remix-templates',
'solhint',
'dgit',
'pinnedPanel',
'pluginStateLogger'
]
@ -247,6 +248,7 @@ export class RemixAppManager extends PluginManager {
const res = await fetch(this.pluginsDirectory)
plugins = await res.json()
plugins = plugins.filter((plugin) => {
if (plugin.name === 'dgit') return false
if (plugin.targets && Array.isArray(plugin.targets) && plugin.targets.length > 0) {
return plugin.targets.includes('remix')
}

@ -10,7 +10,7 @@ export class RemixEngine extends Engine {
setPluginOption ({ name, kind }) {
if (kind === 'provider') return { queueTimeout: 60000 * 2 }
if (name === 'LearnEth') return { queueTimeout: 60000 }
if (name === 'dGitProvider') return { queueTimeout: 60000 * 4 }
if (name === 'dgitApi') return { queueTimeout: 60000 * 4 }
if (name === 'slither') return { queueTimeout: 60000 * 4 } // Requires when a solc version is installed
if (name === 'hardhat') return { queueTimeout: 60000 * 4 }
if (name === 'truffle') return { queueTimeout: 60000 * 4 }

@ -6,6 +6,7 @@ import git from 'isomorphic-git'
import { dialog } from "electron";
import http from 'isomorphic-git/http/web'
import { gitProxy } from "../tools/git";
import { remote } from "../types";
const profile: Profile = {
name: 'isogit',
@ -328,9 +329,9 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
async clone(cmd: any) {
if (this.gitIsInstalled) {
try{
try {
await gitProxy.clone(cmd.url, cmd.dir)
}catch(e){
} catch (e) {
throw e
}
} else {
@ -376,8 +377,9 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
if (!this.workingDir || this.workingDir === '') {
return []
}
let remotes = []
remotes = await git.listRemotes({ ...await this.getGitConfig() })
let remotes: remote[] = []
remotes = (await git.listRemotes({ ...await this.getGitConfig() })).map((remote) => { return { name: remote.remote, url: remote.url } }
)
return remotes
}
@ -408,10 +410,10 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
for (const remote of remotes) {
cmd = {
...cmd,
remote: remote.remote
remote: remote.name
}
const remotebranches = (await git.listBranches(cmd)).map((branch) => { return { remote: remote.remote, name: branch } })
const remotebranches = (await git.listBranches(cmd)).map((branch) => { return { remote: remote.name, name: branch } })
branches = [...branches, ...remotebranches]
}

@ -0,0 +1,9 @@
export type branch = {
name: string
remote: remote
}
export type remote = {
name: string
url: string
}

@ -5,9 +5,11 @@ import {PluginClient} from '@remixproject/plugin'
import {Contract, compileContract} from './compiler'
import {ExampleContract} from '../components/VyperResult'
import EventEmitter from 'events'
import { Plugin } from "@remixproject/engine";
import { CustomRemixApi } from '@remix-api'
export type VyperComplierAddress = 'https://vyper2.remixproject.org/' | 'http://localhost:8000/'
export class RemixClient extends PluginClient {
export class RemixClient extends PluginClient<any, CustomRemixApi> {
private client = createClient<Api, Readonly<RemixApi>>(this)
compilerUrl: VyperComplierAddress = 'https://vyper2.remixproject.org/'
compilerOutput: any
@ -80,15 +82,23 @@ export class RemixClient extends PluginClient {
async cloneVyperRepo() {
try {
// @ts-ignore
this.call('notification', 'toast', 'cloning Snekmate Vyper repository...')
await this.call('manager', 'activatePlugin', 'dGitProvider')
this.call('notification', 'toast', 'cloning Snekmate Vyper repository...')
await this.call(
'dGitProvider',
'dgitApi',
'clone',
{url: 'https://github.com/pcaversaccio/snekmate', token: null, branch: 'v0.0.5'},
// @ts-ignore
'snekmate'
{url: 'https://github.com/pcaversaccio/snekmate', token: null, branch: 'main', singleBranch: false, workspaceName: 'snekmate'},
)
await this.call(
'dgitApi',
'checkout',
{
ref:'v0.0.5',
force: true,
refresh: true,
}
)
this.call(
// @ts-ignore
'notification',

@ -6,8 +6,8 @@ import App from './app/app'
const container = document.getElementById('root');
if (container) {
createRoot(container).render(<React.StrictMode>
createRoot(container).render(
<App />
</React.StrictMode>);
);
}

@ -0,0 +1 @@
export * from './lib/remix-api'

@ -0,0 +1,11 @@
import { StatusEvents } from "@remixproject/plugin-utils"
export interface IConfigApi {
events: {
configChanged: () => void
} & StatusEvents,
methods: {
getAppParameter(key: string): Promise<any>,
setAppParameter(key: string, value: any): Promise<void>
}
}

@ -0,0 +1,10 @@
import { commitChange } from "@remix-ui/git";
import { IFileSystem } from "@remixproject/plugin-api"
// Extended interface with 'diff' method
export interface IExtendedFileSystem extends IFileSystem {
methods: IFileSystem['methods'] & {
/** Compare the differences between two files */
diff(change: commitChange): Promise<void>
};
}

@ -0,0 +1,11 @@
import { fileDecoration } from '@remix-ui/file-decorators'
import { StatusEvents } from '@remixproject/plugin-utils'
export interface IFileDecoratorApi {
events: {
} & StatusEvents
methods: {
clearFileDecorators(path?: string): void
setFileDecorators(decorators: fileDecoration[]): void
}
}

@ -0,0 +1,31 @@
import { ModalTypes } from "@remix-ui/app"
import { StatusEvents } from "@remixproject/plugin-utils"
export interface INotificationApi {
events: {
} & StatusEvents,
methods: {
toast(key: string): Promise<void>,
alert({
title,
message,
id
}:{
title: string,
message: string,
id: string
}): Promise<void>,
modal({
title,
message,
okLabel,
type
}:{
title: string,
message: string,
okLabel: string,
type: ModalTypes
}): Promise<void>,
}
}

@ -0,0 +1,11 @@
import { StatusEvents } from '@remixproject/plugin-utils'
export interface ISettings {
events: {
configChanged: () => void,
} & StatusEvents
methods: {
getGithubAccessToken(): string
get(key: string): Promise<any>
}
}

@ -0,0 +1,19 @@
import { IGitApi } from "@remix-ui/git"
import { IRemixApi } from "@remixproject/plugin-api"
import { StatusEvents } from "@remixproject/plugin-utils"
import { IConfigApi } from "./plugins/config-api"
import { IFileDecoratorApi } from "./plugins/filedecorator-api"
import { IExtendedFileSystem } from "./plugins/fileSystem-api"
import { INotificationApi } from "./plugins/notification-api"
import { ISettings } from "./plugins/settings-api"
export interface ICustomRemixApi extends IRemixApi {
dgitApi: IGitApi
config: IConfigApi
notification: INotificationApi
settings: ISettings
fileDecorator: IFileDecoratorApi
fileManager: IExtendedFileSystem
}
export declare type CustomRemixApi = Readonly<ICustomRemixApi>

@ -1,20 +1,21 @@
import { monacoTypes } from '@remix-ui/editor';
import { commitChange } from '@remix-ui/git';
export interface Action {
type: string;
payload: Record<string, any>
monaco: any,
editor: any
editors: any[]
}
export const initialState = {}
export const reducerActions = (models = initialState, action: Action) => {
const monaco = action.monaco
const editor = action.editor
const editors = action.editors as any[]
switch (action.type) {
case 'ADD_MODEL': {
if (!editor) return models
if (!editors) return models
const uri = action.payload.uri
const value = action.payload.value
const language = action.payload.language
@ -22,6 +23,7 @@ export const reducerActions = (models = initialState, action: Action) => {
if (models[uri]) return models // already existing
models[uri] = { language, uri, readOnly }
let model
try {
model = monaco.editor.createModel(value, language, monaco.Uri.parse(uri))
} catch (e) {
@ -38,8 +40,12 @@ export const reducerActions = (models = initialState, action: Action) => {
delete models[uri]
return models
}
case 'ADD_DIFF': {
if (!editors) return models
return models
}
case 'SET_VALUE': {
if (!editor) return models
if (!editors) return models
const uri = action.payload.uri
const value = action.payload.value
const model = models[uri]?.model
@ -49,15 +55,19 @@ export const reducerActions = (models = initialState, action: Action) => {
return models
}
case 'REVEAL_LINE': {
if (!editor) return models
if (!editors) return models
const line = action.payload.line
const column = action.payload.column
editor.revealLine(line)
editor.setPosition({ column, lineNumber: line })
editors.map((editor) => {
editor.revealLine(line)
editor.setPosition({ column, lineNumber: line })
})
return models
}
case 'REVEAL_RANGE': {
if (!editor) return models
if (!editors) return models
const range: monacoTypes.IRange = {
startLineNumber: action.payload.startLineNumber + 1,
startColumn: action.payload.startColumn,
@ -66,48 +76,60 @@ export const reducerActions = (models = initialState, action: Action) => {
}
// reset to start of line
if (action.payload.startColumn < 100) {
editor.revealRange({
editors.map(editor => editor.revealRange({
startLineNumber: range.startLineNumber,
startColumn: 1,
endLineNumber: range.endLineNumber,
endColumn: 1
})
}))
} else {
editor.revealRangeInCenter(range)
editors.map(editor => editor.revealRangeInCenter(range))
}
return models
}
case 'FOCUS': {
if (!editor) return models
editor.focus()
if (!editors) return models
editors.map(editor => editor.focus())
return models
}
case 'SET_FONTSIZE': {
if (!editor) return models
if (!editors) return models
const size = action.payload.size
if (size === 1) {
editor.trigger('keyboard', 'editor.action.fontZoomIn', {});
} else {
editor.trigger('keyboard', 'editor.action.fontZoomOut', {});
}
editors.map((editor) => {
if (size === 1) {
editor.trigger('keyboard', 'editor.action.fontZoomIn', {});
} else {
editor.trigger('keyboard', 'editor.action.fontZoomOut', {});
}
})
return models
}
case 'SET_WORDWRAP': {
if (!editor) return models
if (!editors) return models
const wrap = action.payload.wrap
editor.updateOptions({ wordWrap: wrap ? 'on' : 'off' })
editors.map(editor =>
editor.updateOptions({ wordWrap: wrap ? 'on' : 'off' }))
return models
}
}
}
export const reducerListener = (plugin, dispatch, monaco, editor, events) => {
export const reducerListener = (plugin, dispatch, monaco, editors: any[], events) => {
plugin.on('editor', 'addModel', (value, language, uri, readOnly) => {
dispatch({
type: 'ADD_MODEL',
payload: { uri, value, language, readOnly, events },
monaco,
editor
editors
})
})
plugin.on('editor', 'addDiff', (value: commitChange) => {
dispatch({
type: 'ADD_DIFF',
payload: { value },
monaco,
editors
})
})
@ -116,7 +138,7 @@ export const reducerListener = (plugin, dispatch, monaco, editor, events) => {
type: 'DISPOSE_MODEL',
payload: { uri },
monaco,
editor
editors
})
})
@ -125,7 +147,7 @@ export const reducerListener = (plugin, dispatch, monaco, editor, events) => {
type: 'SET_VALUE',
payload: { uri, value },
monaco,
editor
editors
})
})
@ -134,7 +156,7 @@ export const reducerListener = (plugin, dispatch, monaco, editor, events) => {
type: 'REVEAL_LINE',
payload: { line, column },
monaco,
editor
editors
})
})
@ -148,7 +170,7 @@ export const reducerListener = (plugin, dispatch, monaco, editor, events) => {
endColumn
},
monaco,
editor
editors
})
})
@ -157,7 +179,7 @@ export const reducerListener = (plugin, dispatch, monaco, editor, events) => {
type: 'FOCUS',
payload: {},
monaco,
editor
editors
})
})
@ -166,7 +188,7 @@ export const reducerListener = (plugin, dispatch, monaco, editor, events) => {
type: 'SET_FONTSIZE',
payload: { size },
monaco,
editor
editors
})
})
@ -175,7 +197,7 @@ export const reducerListener = (plugin, dispatch, monaco, editor, events) => {
type: 'SET_WORDWRAP',
payload: { wrap },
monaco,
editor
editors
})
})
}

@ -1,7 +1,7 @@
import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line
import { FormattedMessage, useIntl } from 'react-intl'
import { isArray } from 'lodash'
import Editor, { loader, Monaco } from '@monaco-editor/react'
import Editor, { DiffEditor, loader, Monaco } from '@monaco-editor/react'
import { AlertModal } from '@remix-ui/app'
import { ConsoleLogs, QueryParams } from '@remix-project/remix-lib'
import { reducerActions, reducerListener, initialState } from './actions/editor'
@ -135,6 +135,8 @@ export interface EditorUIProps {
activated: boolean
themeType: string
currentFile: string
currentDiffFile: string
isDiff: boolean
events: {
onBreakPointAdded: (file: string, line: number) => void
onBreakPointCleared: (file: string, line: number) => void
@ -147,6 +149,8 @@ export interface EditorUIProps {
export const EditorUI = (props: EditorUIProps) => {
const intl = useIntl()
const [, setCurrentBreakpoints] = useState({})
const [isDiff, setIsDiff] = useState(false)
const [isSplit, setIsSplit] = useState(true)
const defaultEditorValue = `
\t\t\t\t\t\t\t ____ _____ __ __ ___ __ __ ___ ____ _____
\t\t\t\t\t\t\t| _ \\ | ____| | \\/ | |_ _| \\ \\/ / |_ _| | _ \\ | ____|
@ -171,6 +175,8 @@ export const EditorUI = (props: EditorUIProps) => {
const pasteCodeRef = useRef(false)
const editorRef = useRef(null)
const monacoRef = useRef<Monaco>(null)
const diffEditorRef = useRef<any>(null)
const currentFunction = useRef('')
const currentFileRef = useRef('')
const currentUrlRef = useRef('')
@ -326,11 +332,19 @@ export const EditorUI = (props: EditorUIProps) => {
})
useEffect(() => {
if (!editorRef.current || !props.currentFile) return
if (!(editorRef.current || diffEditorRef.current ) || !props.currentFile) return
currentFileRef.current = props.currentFile
props.plugin.call('fileManager', 'getUrlFromPath', currentFileRef.current).then((url) => (currentUrlRef.current = url.file))
const file = editorModelsState[props.currentFile]
props.isDiff && diffEditorRef && diffEditorRef.current && diffEditorRef.current.setModel({
original: editorModelsState[props.currentDiffFile].model,
modified: file.model
})
props.isDiff && diffEditorRef.current.getModifiedEditor().updateOptions({ readOnly: editorModelsState[props.currentFile].readOnly })
editorRef.current.setModel(file.model)
editorRef.current.updateOptions({
readOnly: editorModelsState[props.currentFile].readOnly,
@ -348,7 +362,7 @@ export const EditorUI = (props: EditorUIProps) => {
} else if (file.language === 'toml') {
monacoRef.current.editor.setModelLanguage(file.model, 'remix-toml')
}
}, [props.currentFile])
}, [props.currentFile, props.isDiff])
const convertToMonacoDecoration = (decoration: lineText | sourceAnnotation | sourceMarker, typeOfDecoration: string) => {
if (typeOfDecoration === 'sourceAnnotationsPerFile') {
@ -520,6 +534,7 @@ export const EditorUI = (props: EditorUIProps) => {
for (const filePath in allMarkersPerfile) {
const model = editorModelsState[filePath]?.model
if (model) {
console.log('MONACO REF CURRENT', monacoRef.current)
monacoRef.current.editor.setModelMarkers(model, from, allMarkersPerfile[filePath])
}
}
@ -545,6 +560,7 @@ export const EditorUI = (props: EditorUIProps) => {
props.editorAPI.getValue = (uri: string) => {
if (!editorRef.current) return
const model = editorModelsState[uri]?.model
if (model) {
return model.getValue()
@ -614,10 +630,21 @@ export const EditorUI = (props: EditorUIProps) => {
}
}
function setReducerListener() {
if (diffEditorRef.current && diffEditorRef.current.getModifiedEditor() && editorRef.current){
reducerListener(props.plugin, dispatch, monacoRef.current, [diffEditorRef.current.getModifiedEditor(), editorRef.current], props.events)
}
}
function handleDiffEditorDidMount(editor: any) {
diffEditorRef.current = editor
setReducerListener()
}
function handleEditorDidMount(editor) {
editorRef.current = editor
defineAndSetTheme(monacoRef.current)
reducerListener(props.plugin, dispatch, monacoRef.current, editorRef.current, props.events)
setReducerListener()
props.events.onEditorMounted()
editor.onMouseUp((e) => {
// see https://microsoft.github.io/monaco-editor/typedoc/enums/editor.MouseTargetType.html
@ -928,8 +955,22 @@ export const EditorUI = (props: EditorUIProps) => {
return (
<div className="w-100 h-100 d-flex flex-column-reverse">
<DiffEditor
originalLanguage={'remix-solidity'}
modifiedLanguage={'remix-solidity'}
original={''}
modified={''}
onMount={handleDiffEditorDidMount}
options={{ readOnly: false, renderSideBySide: isSplit }}
width='100%'
height={props.isDiff ? '100%' : '0%'}
className={props.isDiff ? "d-block" : "d-none"}
/>
<Editor
width="100%"
height={props.isDiff ? '0%' : '100%'}
path={props.currentFile}
language={editorModelsState[props.currentFile] ? editorModelsState[props.currentFile].language : 'text'}
onMount={handleEditorDidMount}
@ -942,6 +983,7 @@ export const EditorUI = (props: EditorUIProps) => {
}
}}
defaultValue={defaultEditorValue}
className={props.isDiff ? "d-none" : "d-block"}
/>
{editorModelsState[props.currentFile]?.readOnly && (
<span className="pl-4 h6 mb-0 w-100 alert-info position-absolute bottom-0 end-0">

@ -0,0 +1,70 @@
import React, { useEffect, useState } from 'react'
import { gitActionsContext, pluginActionsContext } from '../state/context'
import { ReadCommitResult } from "isomorphic-git"
import { gitPluginContext } from './gitui'
export const BranchHeader = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const pluginActions = React.useContext(pluginActionsContext)
const [changed, setChanged] = useState(false)
const [isDetached, setIsDetached] = useState(false)
const [latestCommit, setLatestCommit] = useState<ReadCommitResult>(null)
useEffect(() => {
if (context.currentBranch) {
actions.getBranchDifferences(context.currentBranch, null, context)
}
if (!context.currentBranch || (context.currentBranch && context.currentBranch.name === '')) {
if (context.currentHead === '') {
setIsDetached(false)
} else {
setIsDetached(true)
}
} else {
setIsDetached(false)
}
setLatestCommit(null)
if (context.currentHead !== '') {
if (context.commits && context.commits.length > 0) {
const commit = context.commits.find(commit => commit.oid === context.currentHead)
if (commit) {
setLatestCommit(commit)
}
}
}
}, [context.currentBranch, context.commits, context.branches, context.remotes, context.currentHead])
useEffect(() => {
if (context.fileStatusResult) {
const total = context.allchangesnotstaged.length
const badges = total + context.staged.length
setChanged((context.deleted.length > 0 || context.staged.length > 0 || context.untracked.length > 0 || context.modified.length > 0))
}
}, [context.fileStatusResult, context.modified, context.allchangesnotstaged, context.untracked, context.deleted])
const showDetachedWarningText = async () => {
await pluginActions.showAlert({
message: `You are in 'detached HEAD' state. This means you are not on a branch because you checkout a tag or a specific commit. If you want to commit changes, you will need to create a new branch.`,
title: 'Warning'
})
}
return (<>
<div className='text-sm w-100'>
<div className='text-secondary long-and-truncated'>
<i className="fa fa-code-branch mr-1 pl-2"></i>
{changed ? '*' : ''}{context.currentBranch && context.currentBranch.name}
</div>
{latestCommit ?
<div className='text-secondary long-and-truncated'>
{latestCommit.commit && latestCommit.commit.message ? latestCommit.commit.message : ''}
</div> : null}
{isDetached ?
<div className='text-warning long-and-truncated'>
You are in a detached state<i onClick={showDetachedWarningText} className="btn fa fa-info-circle mr-1 pl-2"></i>
</div> : null}
</div>
<hr></hr>
</>)
}

@ -0,0 +1,147 @@
import React, { useEffect } from "react"
import { useState } from "react"
import { gitActionsContext } from "../../state/context"
import { gitPluginContext } from "../gitui"
import { faArrowDown, faArrowUp, faCheck, faCloudArrowUp, faSync } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { syncStateContext } from "./sourceControlBase";
enum buttonStateValues {
Commit,
Sync = 1,
PublishBranch = 2
}
export const CommitMessage = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const syncState = React.useContext(syncStateContext)
const [buttonState, setButtonState] = useState<buttonStateValues>(buttonStateValues.Commit)
const [message, setMessage] = useState({ value: '' })
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setMessage({ value: e.currentTarget.value })
}
const commit = async () => {
if (context.staged.length === 0 && context.allchangesnotstaged.length == 0) return
if (context.staged.length === 0)
await actions.addall(context.allchangesnotstaged)
await actions.commit(message.value)
setMessage({ value: '' })
}
const getRemote = () => {
return context.upstream ? context.upstream : context.defaultRemote ? context.defaultRemote : null
}
const sync = async () => {
await actions.pull({
remote: getRemote(),
ref: context.currentBranch
})
await actions.push({
remote: getRemote(),
ref: context.currentBranch
})
await actions.pull({
remote: getRemote(),
ref: context.currentBranch
})
}
const commitNotAllowed = () => {
return context.canCommit === false || message.value === "" || (context.staged.length === 0 && context.allchangesnotstaged.length == 0)
}
const commitMessagePlaceholder = () => {
if (context.currentBranch === undefined || context.currentBranch.name === "")
return `message`
return `message ( commit on ${context.currentBranch.name} )`
}
const syncEnabled = () => {
return syncState.commitsAhead.length > 0 || syncState.commitsBehind.length > 0
}
const upDownArrows = () => {
return (
<>
{syncState.commitsBehind && syncState.commitsBehind.length ? <>{syncState.commitsBehind.length}<FontAwesomeIcon icon={faArrowDown} className="ml-1" /></> : null}
{syncState.commitsAhead && syncState.commitsAhead.length ? <>{syncState.commitsAhead.length}<FontAwesomeIcon icon={faArrowUp} className="ml-1" /></> : null}
</>
)
}
const publishEnabled = () => {
const remoteEquivalentBranch = context.branches.find((b) => b.name === context.currentBranch.name && b.remote)
return remoteEquivalentBranch === undefined && getRemote() !== null
}
const publishBranch = async () => {
if (context.currentBranch === undefined || context.currentBranch.name === "")
return
await actions.push({
remote: getRemote(),
ref: context.currentBranch
})
await actions.fetch({
remote: getRemote(),
ref: context.currentBranch,
singleBranch: false,
relative: true
})
}
const messageEnabled = () => {
return context.canCommit && (context.allchangesnotstaged.length > 0 || context.staged.length > 0)
}
const setButtonStateValues = () => {
if (!commitNotAllowed() || context.allchangesnotstaged.length > 0 || context.staged.length > 0) {
if (context.allchangesnotstaged.length == 0 && context.staged.length == 0 && message.value === "" && publishEnabled()) {
setButtonState(buttonStateValues.PublishBranch)
return
}
setButtonState(buttonStateValues.Commit)
return
}
if (syncEnabled()) {
setButtonState(buttonStateValues.Sync)
return
}
if (publishEnabled()) {
setButtonState(buttonStateValues.PublishBranch)
return
}
setButtonState(buttonStateValues.Commit)
}
useEffect(() => {
setButtonStateValues()
}, [context.canCommit, context.staged, context.allchangesnotstaged, context.currentBranch, syncState.commitsAhead, syncState.commitsBehind, message.value])
return (
<>
<div className="form-group">
<input placeholder={commitMessagePlaceholder()} data-id='commitMessage' disabled={!messageEnabled()} className="form-control" type="text" onChange={handleChange} value={message.value} />
</div>
<button data-id='commitButton' className={`btn btn-primary w-100 ${buttonState === buttonStateValues.Commit ? '' : 'd-none'}`} disabled={commitNotAllowed()} onClick={async () => await commit()} >
<FontAwesomeIcon icon={faCheck} className="mr-1" />
Commit
</button>
<button data-id='syncButton' className={`btn btn-primary w-100 ${buttonState === buttonStateValues.Sync ? '' : 'd-none'}`} disabled={!syncEnabled()} onClick={async () => await sync()} >
<FontAwesomeIcon icon={faSync} className="mr-1" aria-hidden="true" />
Sync Changes {upDownArrows()}
</button>
<button data-id='publishBranchButton' className={`btn btn-primary w-100 ${buttonState === buttonStateValues.PublishBranch ? '' : 'd-none'}`} onClick={async () => await publishBranch()} >
<FontAwesomeIcon icon={faCloudArrowUp} className="mr-1" aria-hidden="true" />
Publish Branch
</button>
<hr></hr>
</>
);
}

@ -0,0 +1,24 @@
import React, { useContext } from 'react'
import { gitPluginContext } from '../gitui'
interface ButtonWithContextProps {
onClick: React.MouseEventHandler<HTMLButtonElement>;
children: React.ReactNode;
disabledCondition?: boolean; // Optional additional disabling condition
// You can add other props if needed, like 'type', 'className', etc.
[key: string]: any; // Allow additional props to be passed
}
// This component extends a button, disabling it when loading is true
const GitUIButton = ({ children, disabledCondition = false, ...rest }:ButtonWithContextProps) => {
const { loading } = React.useContext(gitPluginContext)
const isDisabled = loading || disabledCondition
return (
<button disabled={isDisabled} {...rest}>
{children}
</button>
);
};
export default GitUIButton;

@ -0,0 +1,90 @@
import { faArrowDown, faArrowUp, faArrowsUpDown, faArrowRotateRight } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { CustomTooltip } from "@remix-ui/helper"
import { ReadCommitResult } from "isomorphic-git"
import React, { createContext, useEffect, useState } from "react"
import { FormattedMessage } from "react-intl"
import { gitActionsContext } from "../../state/context"
import { branch, remote } from "../../types"
import { gitPluginContext } from "../gitui"
import GitUIButton from "./gituibutton"
interface SourceControlButtonsProps {
remote?: remote,
branch?: branch,
children: React.ReactNode
}
export const syncStateContext = createContext<{
commitsAhead: ReadCommitResult[],
commitsBehind: ReadCommitResult[]
branch: branch,
remote: remote
}>
({ commitsAhead: [], commitsBehind: [], branch: undefined, remote: undefined })
export const SourceControlBase = (props: SourceControlButtonsProps) => {
const [branch, setBranch] = useState(props.branch)
const [remote, setRemote] = useState(props.remote)
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [commitsAhead, setCommitsAhead] = useState<ReadCommitResult[]>([])
const [commitsBehind, setCommitsBehind] = useState<ReadCommitResult[]>([])
useEffect(() => {
setDefaultRemote()
if (remote && branch && context.branchDifferences && context.branchDifferences[`${remote.name}/${branch.name}`]) {
setCommitsAhead(context.branchDifferences[`${remote.name}/${branch.name}`]?.uniqueHeadCommits)
setCommitsBehind(context.branchDifferences[`${remote.name}/${branch.name}`]?.uniqueRemoteCommits)
} else {
setCommitsAhead([])
setCommitsBehind([])
}
}, [context.branchDifferences, context.currentBranch, branch, remote])
const setDefaultRemote = () => {
if (context.remotes.length > 0) {
// find remote called origin
const origin = context.remotes.find(remote => remote.name === 'origin')
if (origin) {
setRemote(origin)
} else {
setRemote(context.remotes[0])
}
return origin
}
return null
}
useEffect(() => {
if (!props.branch) {
setBranch(context.currentBranch)
}
if (!props.remote) {
setRemote(context.defaultRemote)
} else {
setDefaultRemote()
}
}, [])
useEffect(() => {
if (!props.branch) {
setBranch(context.currentBranch)
}
if (!props.remote) {
setRemote(context.defaultRemote)
} else {
setDefaultRemote()
}
}, [context.defaultRemote, context.currentBranch])
return (<>
<syncStateContext.Provider value={{ commitsAhead, commitsBehind, branch, remote }}>
{props.children}
</syncStateContext.Provider>
</>)
}

@ -0,0 +1,98 @@
import { faArrowDown, faArrowUp, faArrowsUpDown, faArrowRotateRight } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { CustomTooltip } from "@remix-ui/helper"
import React, { useEffect, useState } from "react"
import { FormattedMessage } from "react-intl"
import { gitActionsContext } from "../../state/context"
import { branch, remote } from "../../types"
import { gitPluginContext } from "../gitui"
import GitUIButton from "./gituibutton"
import { syncStateContext } from "./sourceControlBase"
export const SourceControlButtons = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const syncState = React.useContext(syncStateContext)
const [branch, setBranch] = useState<branch>(syncState.branch)
const [remote, setRemote] = useState<remote>(syncState.remote)
const getRemote = () => {
return remote ? remote : context.upstream ? context.upstream : context.defaultRemote ? context.defaultRemote : null
}
const getRemoteName = () => {
return getRemote() ? getRemote().name : ''
}
const pull = async () => {
await actions.pull({
remote: getRemote(),
ref: branch ? branch : context.currentBranch
})
}
const push = async () => {
await actions.push({
remote: getRemote(),
ref: branch ? branch : context.currentBranch
})
await actions.fetch({
remote: getRemote(),
ref: branch ? branch : context.currentBranch,
relative: false,
depth: 1,
singleBranch: true
})
}
const sync = async () => {
await pull()
await push()
}
const refresh = async() => {
await actions.getFileStatusMatrix(null)
await actions.gitlog()
}
const buttonsDisabled = () => {
return (!context.upstream) || context.remotes.length === 0
}
const getTooltipText = (id: string) => {
if (buttonsDisabled()) return <FormattedMessage id="git.noremote" />
return <><FormattedMessage id={id} /> {getRemoteName()}</>
}
return (
<span className='d-flex justify-content-end align-items-center'>
<CustomTooltip tooltipText={getTooltipText('git.pull')}>
<GitUIButton data-id='sourcecontrol-button-pull' disabledCondition={buttonsDisabled()} onClick={pull} className='btn btn-sm pl-0 pr-2'>
<div className="d-flex align-items-baseline">
{syncState.commitsBehind.length ? <div className="badge badge-pill pl-0">
{syncState.commitsBehind.length}
</div> : null}
<FontAwesomeIcon icon={faArrowDown} className="" />
</div>
</GitUIButton>
</CustomTooltip>
<CustomTooltip tooltipText={getTooltipText('git.push')}>
<GitUIButton data-id='sourcecontrol-button-push' disabledCondition={buttonsDisabled()} onClick={push} className='btn btn-sm pl-0 pr-2'>
<div className="d-flex align-items-baseline">
{syncState.commitsAhead.length ? <div className="badge badge-pill pl-0">
{syncState.commitsAhead.length}
</div> : null}
<FontAwesomeIcon icon={faArrowUp} className="" />
</div>
</GitUIButton>
</CustomTooltip>
<CustomTooltip tooltipText={getTooltipText('git.sync')}>
<GitUIButton data-id='sourcecontrol-button-sync' disabledCondition={buttonsDisabled()} onClick={sync} className='btn btn-sm pl-0 pr-2'><FontAwesomeIcon icon={faArrowsUpDown} className="" /></GitUIButton>
</CustomTooltip>
<CustomTooltip tooltipText={<FormattedMessage id="git.refresh" />}>
<GitUIButton onClick={refresh} className='btn btn-sm'><FontAwesomeIcon icon={faArrowRotateRight} className="" /></GitUIButton>
</CustomTooltip>
</span>
)
}

@ -0,0 +1,12 @@
import React, { useEffect, useState } from 'react'
export const Disabled = () => {
return (
<div data-id='disabled' className='text-sm w-100 alert alert-warning'>
Git is currently disabled.<br></br>
If you are using RemixD you can use git on the terminal.<br></br>
</div>
)
}

@ -0,0 +1,50 @@
import React, { useState, useCallback, useEffect } from 'react';
import Select from 'react-select';
import { gitActionsContext } from '../../state/context';
import { selectStyles, selectTheme } from '../../types/styles';
import { gitPluginContext } from '../gitui';
interface BranchySelectProps {
select: (branch:{ name: string }) => void;
}
export const BranchSelect = (props: BranchySelectProps) => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [branchOptions, setBranchOptions] = useState<any>([]);
useEffect(() => {
if (context.remoteBranches && context.remoteBranches.length > 0) {
const options = context.remoteBranches && context.remoteBranches.length > 0 && context.remoteBranches.map(branch => {
return { value: branch.name, label: branch.name }
}
)
setBranchOptions(options)
} else {
setBranchOptions(null)
}
}, [context.remoteBranches])
const selectRemoteBranch = async (e: any) => {
if (!e || !e.value) {
props.select(null)
return
}
const value = e && e.value
props.select({ name: value.toString() })
}
return (<>{branchOptions && branchOptions.length ?
<Select
options={branchOptions}
className="mt-1"
id="branch-select"
onChange={(e: any) =>selectRemoteBranch(e)}
theme={selectTheme}
styles={selectStyles}
isClearable={true}
placeholder="Type to search for a branch..."
/> : null}
</>)
}

@ -0,0 +1,131 @@
import React, { useEffect } from "react";
import { gitActionsContext, pluginActionsContext } from "../../state/context";
import { gitPluginContext } from "../gitui";
import axios from "axios";
import { CopyToClipboard } from "@remix-ui/clipboard";
import { Card } from "react-bootstrap";
export const GetDeviceCode = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const pluginActions = React.useContext(pluginActionsContext)
const [gitHubResponse, setGitHubResponse] = React.useState<any>(null)
const [authorized, setAuthorized] = React.useState<boolean>(false)
const getDeviceCodeFromGitHub = async () => {
setAuthorized(false)
// Send a POST request
const response = await axios({
method: 'post',
url: 'https://github.remixproject.org/login/device/code',
data: {
client_id: '2795b4e41e7197d6ea11',
scope: 'repo gist user:email read:user'
},
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
// convert response to json
const githubrespone = await response.data;
setGitHubResponse(githubrespone)
}
const connectApp = async () => {
// poll https://github.com/login/oauth/access_token
const accestokenresponse = await axios({
method: 'post',
url: 'https://github.remixproject.org/login/oauth/access_token',
data: {
client_id: '2795b4e41e7197d6ea11',
device_code: gitHubResponse.device_code,
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
},
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
// convert response to json
const response = await accestokenresponse.data;
if (response.error) {
}
if (response.access_token) {
setAuthorized(true)
await pluginActions.saveToken(response.access_token)
await actions.loadGitHubUserFromToken()
}
}
const disconnect = async () => {
setAuthorized(false)
setGitHubResponse(null)
await pluginActions.saveToken(null)
await actions.loadGitHubUserFromToken()
}
return (
<>
{(context.gitHubUser && context.gitHubUser.login) ? null :
<button className='btn btn-primary mt-1 w-100' onClick={async () => {
getDeviceCodeFromGitHub();
}}><i className="fab fa-github mr-1"></i>Login in with github</button>
}
{gitHubResponse && !authorized &&
<div className="pt-2">
Step 1: Copy this code:
<div className="input-group text-secondary mb-0 h6">
<input disabled type="text" className="form-control" value={gitHubResponse.user_code} />
<div className="input-group-append">
<CopyToClipboard content={gitHubResponse.user_code} data-id='copyToClipboardCopyIcon' className='far fa-copy ml-1 p-2 mt-1' direction={"top"} />
</div>
</div>
<br></br>
Step 2: Authorize the app here
<br></br><a target="_blank" href={gitHubResponse.verification_uri}>{gitHubResponse.verification_uri}</a>
<br /><br></br>
Step 3: When you are done, click on the button below:
<button className='btn btn-primary mt-1 w-100' onClick={async () => {
connectApp()
}}>Connect</button>
</div>
}
{
(context.gitHubUser && context.gitHubUser.login) ?
<div className="pt-2">
<button className='btn btn-primary mt-1 w-100' onClick={async () => {
disconnect()
}}>Disconnect</button>
</div> : null
}
{
(context.gitHubUser && context.gitHubUser.login) ?
<div className="pt-2">
<Card>
<Card.Body>
<Card.Title data-id={`connected-as-${context.gitHubUser.login}`}>Connected as {context.gitHubUser.login}</Card.Title>
<Card.Text>
<img data-id={`connected-img-${context.gitHubUser.login}`} src={context.gitHubUser.avatar_url} className="w-100" />
<a data-id={`connected-link-${context.gitHubUser.login}`} href={context.gitHubUser.html_url}>{context.gitHubUser.html_url}</a>
{context.userEmails && context.userEmails.filter((email: any) => email.primary).map((email: any) => {
return <span key={email.email}><br></br>{email.email}</span>
})}
</Card.Text>
</Card.Body>
</Card>
</div> : null
}
</>)
}

@ -0,0 +1,87 @@
import React, { useState, useEffect } from 'react';
import { Button } from 'react-bootstrap';
import Select from 'react-select';
import { gitActionsContext } from '../../state/context';
import { repository } from '../../types';
import { selectStyles, selectTheme } from '../../types/styles';
import { gitPluginContext } from '../gitui';
import { TokenWarning } from '../panels/tokenWarning';
interface RepositorySelectProps {
select: (repo: repository) => void;
}
const RepositorySelect = (props: RepositorySelectProps) => {
const [repoOtions, setRepoOptions] = useState<any>([]);
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [loading, setLoading] = useState(false)
const [show, setShow] = useState(false)
useEffect(() => {
if (context.repositories && context.repositories.length > 0) {
// map context.repositories to options
const options = context.repositories && context.repositories.length > 0 && context.repositories.map(repo => {
return { value: repo.id, label: repo.full_name }
})
setRepoOptions(options)
setShow(options.length > 0)
} else {
setRepoOptions(null)
setShow(false)
}
setLoading(false)
}, [context.repositories])
const selectRepo = async (e: any) => {
if (!e || !e.value) {
props.select(null)
return
}
const value = e && e.value
const repo = context.repositories.find(repo => {
return repo.id.toString() === value.toString()
})
if (repo) {
props.select(repo)
await actions.remoteBranches(repo.owner.login, repo.name)
}
}
const fetchRepositories = async () => {
try {
setShow(true)
setLoading(true)
setRepoOptions([])
await actions.repositories()
} catch (e) {
// do nothing
}
};
return (
<><Button data-id='fetch-repositories' onClick={fetchRepositories} className="w-100 mt-1">
<i className="fab fa-github mr-1"></i>Fetch Repositories from GitHub
</Button>
{
show ?
<Select
options={repoOtions}
className="mt-1"
id="repository-select"
onChange={(e: any) => selectRepo(e)}
theme={selectTheme}
styles={selectStyles}
isClearable={true}
placeholder="Type to search for a repository..."
isLoading={loading}
/> : null
}</>
);
};
export default RepositorySelect;

@ -0,0 +1,59 @@
import React, { useEffect, useState } from "react";
import { gitActionsContext } from "../../state/context";
import { repository } from "../../types";
import { gitPluginContext } from "../gitui";
import RepositorySelect from "./repositoryselect";
import { BranchSelect } from "./branchselect";
import { TokenWarning } from "../panels/tokenWarning";
interface RepositoriesProps {
cloneDepth?: number
cloneAllBranches?: boolean
}
export const SelectAndCloneRepositories = (props: RepositoriesProps) => {
const { cloneDepth, cloneAllBranches } = props
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [branch, setBranch] = useState({ name: "" });
const [repo, setRepo] = useState<repository>(null);
const selectRemoteBranch = async (branch:{ name: string }) => {
setBranch(branch)
}
const selectRepo = async (repo: repository) => {
setBranch(null)
setRepo(repo)
}
const clone = async () => {
try {
await actions.clone({
url: repo.html_url,
branch: branch.name,
depth: cloneDepth,
singleBranch: !cloneAllBranches
})
//actions.clone(repo.html_url, branch.name, cloneDepth, !cloneAllBranches)
} catch (e) {
// do nothing
}
};
return (
<>
<TokenWarning />
<RepositorySelect select={selectRepo} />
{repo &&<BranchSelect select={selectRemoteBranch} />}
{repo && branch && branch.name && branch.name !== '0' ?
<button data-id={`clonebtn-${repo.full_name}-${branch.name}`} className='btn btn-primary mt-1 w-100' onClick={async () => {
await clone()
}}>clone {repo.full_name}:{branch.name}</button> : null}
</>
)
}

@ -0,0 +1,245 @@
import React, { useEffect, useReducer, useState } from 'react'
import { add, addall, checkout, checkoutfile, clone, commit, createBranch, remoteBranches, repositories, rm, getCommitChanges, diff, resolveRef, getBranchCommits, setUpstreamRemote, loadGitHubUserFromToken, getBranches, getRemotes, remoteCommits, saveGitHubCredentials, getGitHubCredentialsFromLocalStorage, fetch, pull, push, setDefaultRemote, addRemote, removeRemote, sendToGitLog, clearGitLog, getBranchDifferences, getFileStatusMatrix, init, showAlert, gitlog } from '../lib/gitactions'
import { loadFiles, setCallBacks } from '../lib/listeners'
import { openDiff, openFile, saveToken, setModifiedDecorator, setPlugin, setUntrackedDecorator, statusChanged } from '../lib/pluginActions'
import { gitActionsContext, pluginActionsContext } from '../state/context'
import { gitReducer } from '../state/gitreducer'
import { defaultGitState, defaultLoaderState, gitState, loaderState } from '../types'
import { Accordion } from "react-bootstrap";
import { CommitMessage } from './buttons/commitmessage'
import { Commits } from './panels/commits'
import { Branches } from './panels/branches'
import { SourceControlNavigation } from './navigation/sourcecontrol'
import { BranchesNavigation } from './navigation/branches'
import { CommitsNavigation } from './navigation/commits'
import '../style/index.css'
import { CloneNavigation } from './navigation/clone'
import { Clone } from './panels/clone'
import { Commands } from './panels/commands'
import { CommandsNavigation } from './navigation/commands'
import { RemotesNavigation } from './navigation/remotes'
import { Remotes } from './panels/remotes'
import { ViewPlugin } from '@remixproject/engine-web'
import { GitHubNavigation } from './navigation/github'
import { loaderReducer } from '../state/loaderReducer'
import { GetDeviceCode } from './github/devicecode'
import { LogNavigation } from './navigation/log'
import LogViewer from './panels/log'
import { SourceControlBase } from './buttons/sourceControlBase'
import { BranchHeader } from './branchHeader'
import { SourceControl } from './panels/sourcontrol'
import { GitHubCredentials } from './panels/githubcredentials'
import { Setup } from './panels/setup'
import { Init } from './panels/init'
import { CustomRemixApi } from "@remix-api";
import { Plugin } from "@remixproject/engine";
import { Disabled } from './disabled'
export const gitPluginContext = React.createContext<gitState>(defaultGitState)
export const loaderContext = React.createContext<loaderState>(defaultLoaderState)
interface IGitUi {
plugin: Plugin<any, CustomRemixApi>
}
export const GitUI = (props: IGitUi) => {
const plugin = props.plugin
const [gitState, gitDispatch] = useReducer(gitReducer, defaultGitState)
const [loaderState, loaderDispatch] = useReducer(loaderReducer, defaultLoaderState)
const [activePanel, setActivePanel] = useState<string>("0")
const [setup, setSetup] = useState<boolean>(false)
const [needsInit, setNeedsInit] = useState<boolean>(true)
const [appLoaded, setAppLoaded] = useState<boolean>(false)
useEffect(() => {
plugin.emit('statusChanged', {
key: 'loading',
type: 'info',
title: 'Loading Git Plugin'
})
setTimeout(() => {
setAppLoaded(true)
}, 2000)
}, [])
useEffect(() => {
if (!appLoaded) return
setCallBacks(plugin, gitDispatch, loaderDispatch, setActivePanel)
setPlugin(plugin, gitDispatch, loaderDispatch)
loaderDispatch({ type: 'plugin', payload: true })
}, [appLoaded])
useEffect(() => {
if (!appLoaded) return
async function checkconfig() {
const username = await plugin.call('settings', 'get', 'settings/github-user-name')
const email = await plugin.call('settings', 'get', 'settings/github-email')
const token = await plugin.call('settings', 'get', 'settings/gist-access-token')
setSetup(!(username && email))
}
checkconfig()
}, [gitState.gitHubAccessToken, gitState.gitHubUser, gitState.userEmails, gitState.commits, gitState.branches])
useEffect(() => {
if (!appLoaded) return
async function setDecorators(gitState: gitState) {
await plugin.call('fileDecorator', 'clearFileDecorators')
await setModifiedDecorator(gitState.modified)
await setUntrackedDecorator(gitState.untracked)
}
setTimeout(() => {
setDecorators(gitState)
})
}, [gitState.fileStatusResult])
useEffect(() => {
if (!appLoaded) return
async function updatestate() {
if (gitState.currentBranch && gitState.currentBranch.remote && gitState.currentBranch.remote.url) {
remoteCommits(gitState.currentBranch.remote.url, gitState.currentBranch.name, 1)
}
}
setTimeout(() => {
updatestate()
})
let needsInit = false
if (!(gitState.currentBranch && gitState.currentBranch.name !== '') && gitState.currentHead === '') {
needsInit = true
}
setNeedsInit(needsInit)
}, [gitState.gitHubUser, gitState.currentBranch, gitState.remotes, gitState.gitHubAccessToken, gitState.currentHead])
const gitActionsProviderValue = {
commit,
addall,
add,
checkoutfile,
rm,
checkout,
createBranch,
clone,
repositories,
remoteBranches,
getCommitChanges,
getBranchCommits,
getBranchDifferences,
diff,
resolveRef,
setUpstreamRemote,
loadGitHubUserFromToken,
getBranches,
getRemotes,
fetch,
pull,
push,
setDefaultRemote,
addRemote,
removeRemote,
sendToGitLog,
clearGitLog,
getFileStatusMatrix,
gitlog,
init
}
const pluginActionsProviderValue = {
statusChanged,
loadFiles,
openFile,
openDiff,
saveToken,
saveGitHubCredentials,
getGitHubCredentialsFromLocalStorage,
showAlert
}
return (
<>{(!gitState.canUseApp) ? <Disabled></Disabled> :
<div className="m-1">
<gitPluginContext.Provider value={gitState}>
<loaderContext.Provider value={loaderState}>
<gitActionsContext.Provider value={gitActionsProviderValue}>
<pluginActionsContext.Provider value={pluginActionsProviderValue}>
<BranchHeader />
{setup && !needsInit ? <Setup></Setup> : null}
{needsInit ? <Init></Init> : null}
{!setup && !needsInit ?
<Accordion activeKey={activePanel} defaultActiveKey="0">
<SourceControlNavigation eventKey="0" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className='bg-light' eventKey="0">
<>
<SourceControlBase><CommitMessage /></SourceControlBase>
<SourceControl />
</>
</Accordion.Collapse>
<hr></hr>
<CommandsNavigation eventKey="1" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className='bg-light' eventKey="1">
<>
<Commands></Commands>
</>
</Accordion.Collapse>
<hr></hr>
<CommitsNavigation title={`COMMITS`} eventKey="3" activePanel={activePanel} callback={setActivePanel} showButtons={true} />
<Accordion.Collapse className='bg-light' eventKey="3">
<>
<Commits />
</>
</Accordion.Collapse>
<hr></hr>
<BranchesNavigation eventKey="2" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className='bg-light' eventKey="2">
<>
<Branches /></>
</Accordion.Collapse>
<hr></hr>
<RemotesNavigation eventKey="5" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className='bg-light' eventKey="5">
<>
<Remotes></Remotes>
</>
</Accordion.Collapse>
<hr></hr>
<CloneNavigation eventKey="4" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className='bg-light' eventKey="4">
<>
<Clone /></>
</Accordion.Collapse>
<hr></hr>
<GitHubNavigation eventKey="7" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className='bg-light' eventKey="7">
<>
<GetDeviceCode></GetDeviceCode>
<hr></hr>
<GitHubCredentials></GitHubCredentials>
</>
</Accordion.Collapse>
<hr></hr>
<LogNavigation eventKey="6" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className='bg-light' eventKey="6">
<>
<LogViewer />
</>
</Accordion.Collapse>
</Accordion>
: null}
</pluginActionsContext.Provider>
</gitActionsContext.Provider>
</loaderContext.Provider>
</gitPluginContext.Provider>
</div>}
</>
)
}

@ -0,0 +1,97 @@
import { faCaretUp, faCaretDown, faCaretRight, faArrowUp, faArrowDown, faArrowRotateRight, faArrowsUpDown, faGlobe, faCheckCircle, faToggleOff, faToggleOn, faSync } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useContext, useEffect } from "react";
import { gitActionsContext } from "../../state/context";
import { branch } from "../../types";
import GitUIButton from "../buttons/gituibutton";
import { gitPluginContext } from "../gitui";
interface BrancheDetailsNavigationProps {
eventKey: string;
activePanel: string;
callback: (eventKey: string) => void;
branch: branch;
checkout: (branch: branch) => void;
}
export const BrancheDetailsNavigation = (props: BrancheDetailsNavigationProps) => {
const { eventKey, activePanel, callback, branch, checkout } = props;
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
const getRemote = () => {
return context.upstream ? context.upstream : context.defaultRemote ? context.defaultRemote : null
}
const openRemote = () => {
const remote = branch.remote || getRemote()
window.open(`${remote.url}/tree/${branch.name}`, '_blank');
}
const reloadBranch = () => {
actions.getBranchCommits(branch, 1)
}
const canFetch = () => {
if (getRemote())
return context.branches.find((b) => b.name === branch.name && b.remote && b.remote.url === getRemote().url) ? true : false
}
const fetchBranch = async () => {
await actions.fetch({
remote: null,
ref: branch,
singleBranch: true,
relative: true
})
//actions.fetch(null, branch.name, null, null, false, true)
}
return (
<>
<div className="d-flex flex-row w-100 mb-2 mt-2">
<div data-id={`branches-${context.currentBranch.name === branch.name ? 'current-' : ''}branch-${branch.name}`} onClick={() => handleClick()} role={'button'} className='pointer d-flex flex-row w-100 commit-navigation'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<i className="fa fa-code-branch ml-1"></i>
<div className={`ml-1 ${context.currentBranch.name === branch.name ? 'text-success' : ''}`}>{branch.name} {branch.remote ? `on ${branch.remote.name}` : ''}</div>
</div>
{context.currentBranch && context.currentBranch.name === branch.name ?
<GitUIButton data-id={`branches-toggle-current-branch-${branch.name}`} className="btn btn-sm p-0 mr-1" onClick={() => { }}>
<FontAwesomeIcon className='pointer text-success' icon={faToggleOff} ></FontAwesomeIcon>
</GitUIButton>
:
<GitUIButton data-id={`branches-toggle-branch-${branch.name}`} className="btn btn-sm p-0 mr-1" onClick={() => checkout(branch)}>
<FontAwesomeIcon icon={faToggleOn}></FontAwesomeIcon>
</GitUIButton>
}
{!branch.remote && canFetch() && <>
<GitUIButton className="btn btn-sm p-0 mr-1 text-muted" onClick={() => fetchBranch()}><FontAwesomeIcon icon={faSync} ></FontAwesomeIcon></GitUIButton>
<GitUIButton className="btn btn-sm p-0 mr-1 text-muted" onClick={() => openRemote()}><FontAwesomeIcon icon={faGlobe} ></FontAwesomeIcon></GitUIButton>
</>}
{branch.remote?.url && <>
<GitUIButton className="btn btn-sm p-0 mr-1 text-muted" onClick={() => reloadBranch()}>
<FontAwesomeIcon icon={faSync} ></FontAwesomeIcon>
</GitUIButton>
</>}
{branch.remote?.url && <>
<GitUIButton className="btn btn-sm p-0 mr-1 text-muted" onClick={() => openRemote()}>
<FontAwesomeIcon icon={faGlobe} ></FontAwesomeIcon>
</GitUIButton>
</>}
</div>
</>
);
}

@ -0,0 +1,32 @@
import { faCaretDown, faCaretRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { } from "react";
import { gitActionsContext, pluginActionsContext } from "../../state/context";
import LoaderIndicator from "./loaderindicator";
export const BranchesNavigation = ({ eventKey, activePanel, callback }) => {
const pluginactions = React.useContext(pluginActionsContext)
const context = React.useContext(gitActionsContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between pt-1 ' + (activePanel === eventKey? 'bg-light': '')}>
<span data-id='branches-panel' onClick={()=>handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="pl-1 nav form-check-label">BRANCHES</label>
<LoaderIndicator></LoaderIndicator>
</span>
</div>
</>
);
}

@ -0,0 +1,29 @@
import { faCaretUp, faCaretDown, faArrowUp, faArrowDown, faArrowRotateRight, faCaretRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useContext, useEffect } from "react";
import LoaderIndicator from "./loaderindicator";
export const CloneNavigation = ({ eventKey, activePanel, callback }) => {
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between pb-1 pt-1 ' + (activePanel === eventKey? 'bg-light': '')}>
<span data-id='clone-panel' onClick={()=>handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="pl-1 nav form-check-label">CLONE</label>
<LoaderIndicator></LoaderIndicator>
</span>
</div>
</>
);
}

@ -0,0 +1,35 @@
import { faCaretUp, faCaretDown, faArrowUp, faArrowDown, faArrowRotateRight, faCaretRight, faCircleCheck, faArrowsUpDown, faSpinner } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useContext, useEffect } from "react";
import { pluginActionsContext } from "../../state/context";
import GitUIButton from "../buttons/gituibutton";
import { SourceControlButtons } from "../buttons/sourcecontrolbuttons";
import LoaderIndicator from "./loaderindicator";
export const CommandsNavigation = ({ eventKey, activePanel, callback }) => {
const pluginactions = React.useContext(pluginActionsContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between ' + (activePanel === eventKey ? 'bg-light' : '')}>
<span data-id='commands-panel' onClick={() => handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="pl-1 nav form-check-label">COMMANDS</label>
<LoaderIndicator></LoaderIndicator>
</span>
</div>
</>
);
}

@ -0,0 +1,37 @@
import { faCaretUp, faCaretDown, faCaretRight, faArrowUp, faArrowDown, faArrowRotateRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useContext, useEffect } from "react";
import { CommitSummary } from "../panels/commits/commitsummary";
import { ReadCommitResult } from "isomorphic-git"
interface CommitDetailsNavigationProps {
commit: ReadCommitResult,
checkout: (oid: string) => void
eventKey: string
activePanel: string
callback: (eventKey: string) => void
isAheadOfRepo: boolean
}
export const CommitDetailsNavigation = (props: CommitDetailsNavigationProps) => {
const { commit, checkout, eventKey, activePanel, callback, isAheadOfRepo } = props;
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div onClick={() => handleClick()} role={'button'} className={`pointer mb-2 mt-2 w-100 d-flex flex-row commit-navigation ${isAheadOfRepo ? 'text-success' : ''}`}>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<CommitSummary isAheadOfRepo={isAheadOfRepo} commit={commit} checkout={checkout}></CommitSummary>
</div>
</>
);
}

@ -0,0 +1,63 @@
import { faCaretDown, faArrowUp, faArrowDown, faArrowRotateRight, faCaretRight, faArrowsUpDown, faCloudArrowUp, faCloudArrowDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CustomTooltip } from "@remix-ui/helper";
import React, { useEffect } from "react";
import { FormattedMessage } from "react-intl";
import { pluginActionsContext } from "../../state/context";
import { branch, remote } from "../../types";
import { SourceControlBase } from "../buttons/sourceControlBase";
import { SourceControlButtons } from "../buttons/sourcecontrolbuttons";
import { gitPluginContext } from "../gitui";
import LoaderIndicator from "./loaderindicator";
export interface CommitsNavigationProps {
title: string,
eventKey: string,
activePanel: string,
callback: (eventKey: string) => void
branch?: branch,
remote?: remote
showButtons?: boolean
ahead?: boolean,
behind?: boolean,
}
export const CommitsNavigation = ({ eventKey, activePanel, callback, title, branch, remote, showButtons, ahead, behind }: CommitsNavigationProps) => {
const pluginactions = React.useContext(pluginActionsContext)
const [pullEnabled, setPullEnabled] = React.useState(true)
const [pushEnabled, setPushEnabled] = React.useState(true)
const [syncEnabled, setSyncEnabled] = React.useState(false)
const [fetchEnabled, setFetchEnabled] = React.useState(true)
const context = React.useContext(gitPluginContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={`d-flex justify-content-between ${activePanel === eventKey ? 'bg-light' : ''} ${ahead || behind? 'text-success':''}`}>
<span data-id={`commits-panel${ahead?'-ahead':''}${behind?'-behind':''}`} onClick={() => handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-100'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
{ahead? <FontAwesomeIcon className='ml-1' icon={faCloudArrowUp}></FontAwesomeIcon> : null}
{behind? <FontAwesomeIcon className='ml-1' icon={faCloudArrowDown}></FontAwesomeIcon> : null}
<label className={`pl-1 nav form-check-label ${ahead || behind? 'text-success':''}`}>{title}</label>
<LoaderIndicator></LoaderIndicator>
</span>
{showButtons ?
<SourceControlBase branch={branch} remote={remote}>
<SourceControlButtons />
</SourceControlBase> : null}
</div>
</>
);
}

@ -0,0 +1,29 @@
import { faCaretDown, faCaretRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { } from "react";
import { pluginActionsContext } from "../../state/context";
export const GitHubNavigation = ({ eventKey, activePanel, callback }) => {
const pluginactions = React.useContext(pluginActionsContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between pt-1 pb-1 ' + (activePanel === eventKey? 'bg-light': '')}>
<span data-id='github-panel' onClick={()=>handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="pl-1 nav form-check-label">GITHUB SETUP</label>
</span>
</div>
</>
);
}

@ -0,0 +1,20 @@
import React, { useContext } from 'react'
import { gitPluginContext } from '../gitui'
interface LoaderIndicatorProps {
type?: string;
isLoadingCondition?: boolean; // Optional additional disabling condition
}
// This component extends a button, disabling it when loading is true
const LoaderIndicator = ({ type, isLoadingCondition }: LoaderIndicatorProps) => {
const { loading } = React.useContext(gitPluginContext)
const isLoading = loading || isLoadingCondition
if (!isLoading) return null
return (
<i style={{ fontSize: 'x-small' }} className="ml-1 fas fa-spinner fa-spin fa-4x"></i>
);
};
export default LoaderIndicator;

@ -0,0 +1,87 @@
import { faBan, faCaretDown, faCaretRight, faCircleCheck, faCircleInfo, faInfo, faTrash, faTriangleExclamation, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useContext, useEffect, useState } from "react";
import { gitActionsContext, pluginActionsContext } from "../../state/context";
import { gitPluginContext } from "../gitui";
export const LogNavigation = ({ eventKey, activePanel, callback }) => {
const context = useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [logState, setLogState] = useState({
errorCount: 0,
warningCount: 0,
infoCount: 0,
successCount: 0
});
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
useEffect(() => {
if (!context.log) return
// count different types of logs
const errorCount = context.log.filter(log => log.type === 'error').length
const warningCount = context.log.filter(log => log.type === 'warning').length
const infoCount = context.log.filter(log => log.type === 'info').length
const successCount = context.log.filter(log => log.type === 'success').length
// update the state
setLogState({
errorCount,
warningCount,
infoCount,
successCount
})
}, [context.log])
const clearLogs = () => {
actions.clearGitLog()
}
return (
<>
<div className={'d-flex justify-content-between pt-1 pb-1 ' + (activePanel === eventKey ? 'bg-light' : '')}>
<span onClick={() => handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="pl-1 nav form-check-label mr-2">LOG</label>
{logState.errorCount > 0 && (
<div className="text-danger mr-1">
{logState.errorCount}
<FontAwesomeIcon className="ml-1" icon={faTriangleExclamation} />
</div>
)}
{logState.warningCount > 0 && (
<div className="text-warning mr-1">
{logState.warningCount}
<FontAwesomeIcon className="ml-1" icon={faWarning} />
</div>
)}
{logState.infoCount > 0 && (
<div className="text-info mr-1">
{logState.infoCount}
<FontAwesomeIcon className="ml-1" icon={faCircleInfo} />
</div>
)}
{logState.successCount > 0 && (
<div className="text-success">
{logState.successCount}
<FontAwesomeIcon className="ml-1" icon={faCircleCheck} />
</div>
)}
</span>
{context.log && context.log.length > 0 && (
<FontAwesomeIcon onClick={clearLogs} className='btn btn-sm' icon={faBan}></FontAwesomeIcon>)}
</div>
</>
);
}

@ -0,0 +1,49 @@
import { count } from "console"
import { CustomIconsToggle, CustomMenu, CustomTooltip } from "@remix-ui/helper"
import React, { useState } from "react"
import { Dropdown } from "react-bootstrap"
import { FormattedMessage } from "react-intl"
export const SourceControlMenu = () => {
const [showIconsMenu, hideIconsMenu] = useState<boolean>(false)
return (
<Dropdown id="workspacesMenuDropdown" data-id="sourceControlMenuDropdown" onToggle={() => hideIconsMenu(!showIconsMenu)} show={showIconsMenu}>
<Dropdown.Toggle
onClick={() => {
hideIconsMenu(!showIconsMenu)
}}
as={CustomIconsToggle}
icon={'fas fa-bars'}
></Dropdown.Toggle>
<Dropdown.Menu as={CustomMenu} data-id="wsdropdownMenu" className='custom-dropdown-items remixui_menuwidth' rootCloseEvent="click">
<Dropdown.Item key={0}>
<CustomTooltip
placement="right-start"
tooltipId="cloneWorkspaceTooltip"
tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id='filePanel.workspace.clone' defaultMessage='Clone Git Repository' />}
>
<div
data-id='cloneGitRepository'
onClick={() => {
hideIconsMenu(!showIconsMenu)
}}
key={`cloneGitRepository-fe-ws`}
>
<span
id='cloneGitRepository'
data-id='cloneGitRepository'
onClick={() => {
hideIconsMenu(!showIconsMenu)
}}
className='fab fa-github pl-2'
>
</span>
<span className="pl-3"><FormattedMessage id='filePanel.clone' defaultMessage='Clone' /></span>
</div>
</CustomTooltip>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
)
}

@ -0,0 +1,32 @@
import { faCaretDown, faCaretRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { } from "react";
import { gitActionsContext, pluginActionsContext } from "../../state/context";
import LoaderIndicator from "./loaderindicator";
export const RemotesNavigation = ({ eventKey, activePanel, callback }) => {
const pluginactions = React.useContext(pluginActionsContext)
const context = React.useContext(gitActionsContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between pt-1 pb-1 ' + (activePanel === eventKey? 'bg-light': '')}>
<span data-id='remotes-panel' onClick={()=>handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="pl-1 nav form-check-label">REMOTES</label>
<LoaderIndicator></LoaderIndicator>
</span>
</div>
</>
);
}

@ -0,0 +1,68 @@
import { faCaretDown, faCaretRight, faArrowRightArrowLeft, faGlobe, faToggleOff, faToggleOn, faTrash, faCheck, faSync } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CustomTooltip } from "@remix-ui/helper";
import React, { useContext, useEffect } from "react";
import { gitActionsContext } from "../../state/context";
import { branch, remote } from "../../types";
import GitUIButton from "../buttons/gituibutton";
import { gitPluginContext } from "../gitui";
interface RemotesDetailsNavigationProps {
eventKey: string;
activePanel: string;
callback: (eventKey: string) => void;
remote: remote;
}
export const RemotesDetailsNavigation = (props: RemotesDetailsNavigationProps) => {
const { eventKey, activePanel, callback, remote } = props;
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
const openRemote = () => {
window.open(`${remote.url}`, '_blank');
}
const setAsDefault = () => {
actions.setDefaultRemote(remote)
}
return (
<>
<div className="d-flex flex-row w-100 mb-2 mt-2">
<div data-id={`remote-detail-${remote.name}${context.defaultRemote && context.defaultRemote?.url === remote.url ? '-default' : ''}`} onClick={() => handleClick()} role={'button'} className='pointer long-and-truncated d-flex flex-row commit-navigation'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<CustomTooltip tooltipText={remote.url} placement="top">
<div className={`long-and-truncated ml-1 ${context.defaultRemote && context.defaultRemote?.url === remote.url ? 'text-success' : ''}`}>
{remote.name} <FontAwesomeIcon className='' icon={faArrowRightArrowLeft}></FontAwesomeIcon> {remote.url}
</div>
</CustomTooltip>
</div>
{context.defaultRemote && context.defaultRemote?.url === remote.url ?
<GitUIButton className="btn btn-sm" onClick={() => { }} disabledCondition={true}><FontAwesomeIcon className='text-success' icon={faCheck} ></FontAwesomeIcon></GitUIButton>
:
<GitUIButton className="btn btn-sm" onClick={setAsDefault}><FontAwesomeIcon icon={faToggleOn}></FontAwesomeIcon></GitUIButton>
}
<GitUIButton data-id={`remote-sync-${remote.name}`} className="btn btn-sm" onClick={async () => {
await actions.fetch({
remote
})
}}><FontAwesomeIcon icon={faSync} ></FontAwesomeIcon></GitUIButton>
<GitUIButton data-id={`remote-rm-${remote.name}`} className="btn btn-sm" onClick={() => actions.removeRemote(remote)}><FontAwesomeIcon className='text-danger' icon={faTrash} ></FontAwesomeIcon></GitUIButton>
{remote?.url && <GitUIButton className="btn btn-sm pr-0" onClick={() => openRemote()}><FontAwesomeIcon icon={faGlobe} ></FontAwesomeIcon></GitUIButton>}
</div>
</>
);
}

@ -0,0 +1,41 @@
import { faCaretUp, faCaretDown, faArrowUp, faArrowDown, faArrowRotateRight, faCaretRight, faArrowsUpDown, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CustomTooltip } from "@remix-ui/helper";
import React, { useContext, useEffect } from "react";
import { FormattedMessage } from "react-intl";
import { pluginActionsContext } from "../../state/context";
export const SettingsNavigation = ({ eventKey, activePanel, callback }) => {
const pluginactions = React.useContext(pluginActionsContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between ' + (activePanel === eventKey ? 'bg-light' : '')}>
<span onClick={() => handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="nav pl-1 form-check-label">SETTINGS</label>
</span>
<span className='d-flex justify-content-end align-items-center w-25'>
<CustomTooltip tooltipText={<FormattedMessage id="Missing values" />}>
<button onClick={async () => { await pluginactions.loadFiles() }} className='btn btn-sm text-warning'><FontAwesomeIcon icon={faTriangleExclamation} className="" /></button>
</CustomTooltip>
</span>
</div>
</>
);
}

@ -0,0 +1,42 @@
import { faCaretUp, faCaretDown, faArrowUp, faArrowDown, faArrowRotateRight, faCaretRight, faArrowsUpDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CustomTooltip } from "@remix-ui/helper";
import React, { useContext, useEffect } from "react";
import { FormattedMessage } from "react-intl";
import { pluginActionsContext } from "../../state/context";
import { SourceControlBase } from "../buttons/sourceControlBase";
import { SourceControlButtons } from "../buttons/sourcecontrolbuttons";
import { gitPluginContext } from "../gitui";
import LoaderIndicator from "./loaderindicator";
import { SourceControlMenu } from "./menu/sourcecontrolmenu";
export const SourceControlNavigation = ({ eventKey, activePanel, callback }) => {
const pluginactions = React.useContext(pluginActionsContext)
const context = React.useContext(gitPluginContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between ' + (activePanel === eventKey ? 'bg-light' : '')}>
<span data-id='sourcecontrol-panel' onClick={() => handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="nav pl-1 form-check-label">SOURCE CONTROL</label>
<LoaderIndicator></LoaderIndicator>
</span>
<SourceControlBase><SourceControlButtons/></SourceControlBase>
</div>
</>
);
}

@ -0,0 +1,52 @@
import { faCaretUp, faCaretDown, faArrowUp, faArrowDown, faArrowRotateRight, faCaretRight, faArrowsUpDown, faPlus, faMinus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CustomTooltip } from "@remix-ui/helper";
import React, { useContext, useEffect } from "react";
import { FormattedMessage } from "react-intl";
import { gitActionsContext, pluginActionsContext } from "../../state/context";
import { sourceControlGroup } from "../../types";
import { gitPluginContext } from "../gitui";
interface SourceControlGroupNavigationProps {
eventKey: string;
activePanel: string;
callback: (eventKey: string) => void;
group: sourceControlGroup
}
export const SourceControlGroupNavigation = (props: SourceControlGroupNavigationProps) => {
const { eventKey, activePanel, callback, group } = props;
const actions = React.useContext(gitActionsContext)
const pluginActions = React.useContext(pluginActionsContext)
const context = React.useContext(gitPluginContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between pt-1 ' + (activePanel === eventKey? 'bg-light': '')}>
<span onClick={()=>handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="pl-1 nav form-check-label">{group.name}</label>
</span>
{
activePanel === eventKey ?
<span className='d-flex justify-content-end align-items-center w-25'>
{group.name === 'Changes' ?
<CustomTooltip tooltipText={<FormattedMessage id="git.stageall" />}>
<button data-id='sourcecontrol-add-all' onClick={async () => { await actions.addall(context.allchangesnotstaged) }} className='btn btn-sm'><FontAwesomeIcon icon={faPlus} className="" /></button>
</CustomTooltip>: null}
</span> : null
}
</div>
</>
);
}

@ -0,0 +1,60 @@
import React, { useEffect, useState } from "react";
import { Alert } from "react-bootstrap";
import { gitActionsContext } from "../../state/context";
import { remote } from "../../types";
import GitUIButton from "../buttons/gituibutton";
import { gitPluginContext } from "../gitui";
import { LocalBranchDetails } from "./branches/localbranchdetails";
import { RemoteBranchDetails } from "./branches/remotebranchedetails";
export const Branches = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [newBranch, setNewBranch] = useState({ value: "" });
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewBranch({ value: e.currentTarget.value });
};
return (
<>
<div data-id='branches-panel-content' className="pt-1">
{context.branches && context.branches.length ?
<div>
{context.branches && context.branches.filter((branch, index) => !branch.remote).map((branch, index) => {
return (
<LocalBranchDetails key={index} branch={branch}></LocalBranchDetails>
);
})}
<hr />
</div> : null}
{context.currentBranch
&& context.currentBranch.name !== ''
&& (!context.branches || context.branches.length === 0) ?
<div className="text-muted">Current branch is `{context.currentBranch.name}` but you have no commits.<hr /></div>
: null}
<label>create branch</label>
<div className="form-group">
<input
placeholder="branch name"
onChange={handleChange}
className="form-control w-md-25 w-100"
data-id="newbranchname"
type="text"
id="newbranchname"
/>
</div>
<GitUIButton
data-id="sourcecontrol-create-branch"
onClick={async () => actions.createBranch(newBranch.value)}
className="btn w-md-25 w-100 btn-primary"
id="createbranch-btn"
>
create new branch
</GitUIButton>
</div>
</>
);
}

@ -0,0 +1,48 @@
import { ReadCommitResult } from "isomorphic-git";
import { Accordion } from "react-bootstrap";
import React, { useEffect, useState } from "react";
import { CommitDetails } from "../commits/commitdetails";
import { CommitsNavigation } from "../../navigation/commits";
import { branch, remote } from "../../../types";
import { gitActionsContext } from "../../../state/context";
import { gitPluginContext } from "../../gitui";
export interface BrancheDifferenceProps {
commits: ReadCommitResult[];
title: string,
remote?: remote,
branch?: branch
ahead?: boolean,
behind?: boolean
}
export const BranchDifferenceDetails = (props: BrancheDifferenceProps) => {
const { commits, title, branch, remote, ahead, behind } = props;
const [activePanel, setActivePanel] = useState<string>("");
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
if (commits.length === 0) return null
const getRemote = () => {
return remote ? remote : context.upstream ? context.upstream : context.defaultRemote ? context.defaultRemote : null
}
const getCommitChanges = async (commit: ReadCommitResult) => {
await actions.getCommitChanges(commit.oid, commit.commit.parent[0], null, getRemote())
}
return (
<Accordion activeKey={activePanel} defaultActiveKey="">
<CommitsNavigation ahead={ahead} behind={behind} branch={branch} remote={remote} title={title} eventKey="0" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className="pl-2 border-left ml-1" eventKey="0">
<div data-id={`branchdifference-commits-${branch.name}${ahead?'-ahead':''}${behind?'-behind':''}`} className="ml-1">
{commits && commits.map((commit, index) => {
return (
<CommitDetails branch={branch} getCommitChanges={getCommitChanges} key={index} checkout={()=>{}} commit={commit}></CommitDetails>
);
})}
</div>
</Accordion.Collapse>
</Accordion>)
}

@ -0,0 +1,39 @@
import { branch, remote } from "../../../types";
import React, { useEffect, useState } from "react";
import { gitPluginContext } from "../../gitui";
import { CommitDetails } from "../commits/commitdetails";
import { BranchDifferenceDetails } from "./branchdifferencedetails";
export interface BrancheDetailsProps {
branch: branch;
showSummary?: boolean;
}
export const BranchDifferences = (props: BrancheDetailsProps) => {
const { branch, showSummary } = props;
const context = React.useContext(gitPluginContext)
const getRemote = (): remote | null => {
return context.upstream ? context.upstream : context.defaultRemote ? context.defaultRemote : null
}
const commitsAhead = (remote: remote) => {
if (!remote) return [];
return context.branchDifferences[`${remote.name}/${branch.name}`]?.uniqueHeadCommits || [];
}
const commitsBehind = (remote: remote) => {
if (!remote) return [];
return context.branchDifferences[`${remote.name}/${branch.name}`]?.uniqueRemoteCommits || [];
}
if (!getRemote()) return null;
return (
<div>
<BranchDifferenceDetails ahead={true} branch={branch} remote={getRemote()} title={`ahead of ${getRemote().name} by ${commitsAhead(getRemote()).length} commit(s)`} commits={commitsAhead(getRemote())}></BranchDifferenceDetails>
<BranchDifferenceDetails behind={true} branch={branch} remote={getRemote()} title={`behind ${getRemote().name} by ${commitsBehind(getRemote()).length} commit(s)`} commits={commitsBehind(getRemote())}></BranchDifferenceDetails>
{commitsAhead(getRemote()).length === 0 && commitsBehind(getRemote()).length === 0 ? null : <hr></hr>}
</div>)
}

@ -0,0 +1,86 @@
import { ReadCommitResult } from "isomorphic-git"
import React, { useEffect, useState } from "react";
import { Accordion } from "react-bootstrap";
import { CommitDetailsNavigation } from "../../navigation/commitdetails";
import { gitActionsContext } from "../../../state/context";
import { gitPluginContext } from "../../gitui";
import { branch } from "../../../types";
import { BrancheDetailsNavigation } from "../../navigation/branchedetails";
import { CommitDetailsItems } from "../commits/commitdetailsitem";
import { CommitDetails } from "../commits/commitdetails";
import { BranchDifferences } from "./branchdifferences";
import GitUIButton from "../../buttons/gituibutton";
export interface BrancheDetailsProps {
branch: branch;
}
export const LocalBranchDetails = (props: BrancheDetailsProps) => {
const { branch } = props;
const actions = React.useContext(gitActionsContext)
const context = React.useContext(gitPluginContext)
const [activePanel, setActivePanel] = useState<string>("");
const [hasNextPage, setHasNextPage] = useState<boolean>(false)
const [lastPageNumber, setLastPageNumber] = useState<number>(0)
useEffect(() => {
if (activePanel === "0") {
if (lastPageNumber === 0)
actions.getBranchCommits(branch, 1)
actions.getBranchDifferences(branch, null, context)
}
}, [activePanel])
const checkout = (branch: branch) => {
actions.checkout({
ref: branch.name,
remote: branch.remote && branch.remote.name || null,
refresh: true
});
}
const loadNextPage = () => {
actions.getBranchCommits(branch, lastPageNumber + 1)
}
const checkoutCommit = async (oid: string) => {
try {
actions.checkout({ ref: oid })
;
} catch (e) {
//
}
};
const getRemote = () => {
return context.upstream ? context.upstream : context.defaultRemote ? context.defaultRemote : null
}
const getCommitChanges = async (commit: ReadCommitResult) => {
await actions.getCommitChanges(commit.oid, commit.commit.parent[0], null, getRemote())
}
return (<Accordion activeKey={activePanel} defaultActiveKey="">
<BrancheDetailsNavigation checkout={checkout} branch={branch} eventKey="0" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className="pl-2 border-left ml-1" eventKey="0">
<>
<div className="ml-1">
<BranchDifferences branch={branch}></BranchDifferences>
<div data-id={`local-branch-commits-${branch && branch.name}`}>
{context.localBranchCommits && Object.entries(context.localBranchCommits).map(([key, value]) => {
if (key == branch.name) {
return value.map((commit, index) => {
return (<CommitDetails branch={branch} key={index} getCommitChanges={getCommitChanges} checkout={checkoutCommit} commit={commit}></CommitDetails>)
})
}
})}
</div>
</div>
{hasNextPage && <GitUIButton className="mb-1 ml-2 btn btn-sm" onClick={loadNextPage}>Load more</GitUIButton>}
</>
</Accordion.Collapse>
</Accordion>)
}

@ -0,0 +1,111 @@
import { ReadCommitResult } from "isomorphic-git"
import React, { useEffect, useState } from "react";
import { Accordion } from "react-bootstrap";
import { CommitDetailsNavigation } from "../../navigation/commitdetails";
import { gitActionsContext } from "../../../state/context";
import { gitPluginContext } from "../../gitui";
import { branch } from "../../../types";
import { BrancheDetailsNavigation } from "../../navigation/branchedetails";
import { CommitDetailsItems } from "../commits/commitdetailsitem";
import { CommitDetails } from "../commits/commitdetails";
import GitUIButton from "../../buttons/gituibutton";
export interface BrancheDetailsProps {
branch: branch;
}
export const RemoteBranchDetails = (props: BrancheDetailsProps) => {
const { branch } = props;
const actions = React.useContext(gitActionsContext)
const context = React.useContext(gitPluginContext)
const [activePanel, setActivePanel] = useState<string>("");
const [hasNextPage, setHasNextPage] = useState<boolean>(false)
const [lastPageNumber, setLastPageNumber] = useState<number>(0)
useEffect(() => {
if (activePanel === "0") {
if (lastPageNumber === 0)
actions.getBranchCommits(branch, 1)
}
}, [activePanel])
useEffect(() => {
let hasNextPage = false
let lastPageNumber = 0
context.remoteBranchCommits && Object.entries(context.remoteBranchCommits).map(([key, value]) => {
if (key == branch.name) {
value.map((page, index) => {
hasNextPage = page.hasNextPage
lastPageNumber = page.page
})
}
})
setHasNextPage(hasNextPage)
setLastPageNumber(lastPageNumber)
}, [context.remoteBranchCommits])
const checkout = async (branch: branch) => {
await actions.fetch({
remote: branch.remote,
ref: branch,
depth: 20,
singleBranch: true,
relative: false,
quiet: true
})
await actions.checkout({
ref: branch.name,
remote: branch.remote && branch.remote.name || null,
refresh: true
});
await actions.getBranches()
}
const loadNextPage = () => {
actions.getBranchCommits(branch, lastPageNumber + 1)
}
const checkoutCommit = async (oid: string) => {
try {
actions.checkout({ ref: oid })
} catch (e) {
//
}
};
const getCommitChanges = async (commit: ReadCommitResult) => {
const changes = await actions.getCommitChanges(commit.oid, commit.commit.parent[0], branch, branch.remote)
if (!changes) {
await actions.fetch({
remote: branch.remote,
ref: branch,
depth: 20,
singleBranch: true,
relative: false,
quiet: true
})
}
}
return (<Accordion activeKey={activePanel} defaultActiveKey="">
<BrancheDetailsNavigation checkout={checkout} branch={branch} eventKey="0" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className="pl-2 border-left ml-1" eventKey="0">
<>
<div data-id={`remote-branch-commits-${branch && branch.name}`} className="ml-1">
{context.remoteBranchCommits && Object.entries(context.remoteBranchCommits).map(([key, value]) => {
if (key == branch.name) {
return value.map((page, index) => {
return page.commits.map((commit, index) => {
return (<CommitDetails branch={branch} getCommitChanges={getCommitChanges} key={index} checkout={checkoutCommit} commit={commit}></CommitDetails>)
})
})
}
})}
</div>
{hasNextPage && <GitUIButton className="mb-1 ml-2 btn btn-sm" onClick={loadNextPage}>Load more</GitUIButton>}
</>
</Accordion.Collapse>
</Accordion>)
}

@ -0,0 +1,102 @@
import React, { useState } from "react";
import { Alert, Form, FormControl, InputGroup } from "react-bootstrap";
import { useLocalStorage } from "../../hooks/useLocalStorage";
import { gitActionsContext } from "../../state/context";
import { gitPluginContext } from "../gitui";
import { SelectAndCloneRepositories } from "../github/selectandclonerepositories";
import { RemixUiCheckbox } from "@remix-ui/checkbox";
import GitUIButton from "../buttons/gituibutton";
export const Clone = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [cloneUrl, setCloneUrl] = useLocalStorage(
"CLONE_URL",
''
);
const [cloneDepth, setCloneDepth] = useLocalStorage(
"CLONE_DEPTH",
1
);
const [cloneBranch, setCloneBranch] = useLocalStorage(
"CLONE_BRANCH",
''
);
const [url, setUrl] = useLocalStorage(
"GITHUB_URL",
''
);
const clone = async () => {
await actions.clone({
url: cloneUrl,
branch: cloneBranch,
depth: cloneDepth,
singleBranch: !cloneAllBranches
})
//await actions.clone(cloneUrl, cloneBranch, cloneDepth, !cloneAllBranches)
}
const onCloneBranchChange = (value: string) => {
setCloneBranch(value)
}
const onGitHubCloneUrlChange = (value: string) => {
setCloneUrl(value)
}
const onDepthChange = (value: number) => {
setCloneDepth(value)
}
const [cloneAllBranches, setcloneAllBranches] = useLocalStorage(
"GITHUB_CLONE_ALL_BRANCES",
false
);
const onAllBranchChange = () => {
setcloneAllBranches((e: any) => !e)
}
return (
<>
<div data-id="clone-panel-content">
<InputGroup className="mb-1">
<FormControl data-id="clone-url" id="cloneulr" placeholder="url" name='cloneurl' value={cloneUrl} onChange={e => onGitHubCloneUrlChange(e.target.value)} aria-describedby="urlprepend" />
</InputGroup>
<input name='clonebranch' onChange={e => onCloneBranchChange(e.target.value)} value={cloneBranch} className="form-control mb-1 mt-2" placeholder="branch" type="text" id="clonebranch" />
<GitUIButton disabledCondition={!cloneUrl} data-id='clone-btn' className='btn btn-primary mt-1 w-100' onClick={async () => {
clone()
}}>clone</GitUIButton>
<hr />
<SelectAndCloneRepositories cloneAllBranches={cloneAllBranches} cloneDepth={cloneDepth} />
<hr />
<label>options</label>
<InputGroup className="mt-1 mb-1">
<InputGroup.Prepend>
<InputGroup.Text id="clonedepthprepend">
--depth
</InputGroup.Text>
</InputGroup.Prepend>
<FormControl id="clonedepth" type="number" value={cloneDepth} onChange={e => onDepthChange(parseInt(e.target.value))} aria-describedby="clonedepthprepend" />
</InputGroup>
<RemixUiCheckbox
id={`cloneAllBranches`}
inputType="checkbox"
name="cloneAllBranches"
label={`Clone all branches`}
onClick={() => onAllBranchChange()}
checked={cloneAllBranches}
onChange={() => { }}
/>
<hr></hr>
</div>
</>)
}

@ -0,0 +1,14 @@
import React, { useEffect, useState } from "react";
import { PushPull } from "./commands/pushpull";
import { Fetch } from "./commands/fetch";
import { Merge } from "./commands/merge";
export const Commands = () => {
return (
<>
<PushPull></PushPull>
<hr></hr>
<Fetch></Fetch>
</>)
}

@ -0,0 +1,25 @@
import React, { useEffect, useState } from "react";
import { gitActionsContext } from "../../../state/context";
import GitUIButton from "../../buttons/gituibutton";
import { gitPluginContext } from "../../gitui";
export const Fetch = () => {
const actions = React.useContext(gitActionsContext)
const context = React.useContext(gitPluginContext)
const fetchIsDisabled = () => {
return (!context.upstream) || context.remotes.length === 0
}
return (
<>
<div className="btn-group w-100" role="group">
<GitUIButton data-id='sourcecontrol-fetch-remote' disabledCondition={fetchIsDisabled()} type="button" onClick={async () => actions.fetch({
remote: context.upstream,
})} className="btn btn-primary mr-1 w-50"><div>Fetch {context.upstream && context.upstream.name}</div></GitUIButton>
<GitUIButton data-id='sourcecontrol-fetch-branch' disabledCondition={fetchIsDisabled()} type="button" onClick={async () => actions.fetch({
remote: context.upstream,
ref: context.currentBranch
})} className="btn btn-primary w-50 long-and-truncated">Fetch {context.currentBranch.name}</GitUIButton>
</div>
</>)
}

@ -0,0 +1,57 @@
import React, { useEffect, useState } from "react";
import { gitActionsContext } from "../../../state/context";
import { gitPluginContext } from "../../gitui";
import { selectStyles, selectTheme } from "../../../types/styles";
import Select from 'react-select'
import GitUIButton from "../../buttons/gituibutton";
export const Merge = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [localBranch, setLocalBranch] = useState('')
const [localBranchOptions, setLocalBranchOptions] = useState<any>([]);
useEffect(() => {
setLocalBranch(context.currentBranch.name)
}, [context.currentBranch])
const onLocalBranchChange = (value: any) => {
setLocalBranch(value)
}
const merge = async () => {
//gitservice.push(currentRemote, branch || '', remoteBranch, force)
}
useEffect(() => {
// map context.repositories to options
const localBranches = context.branches && context.branches.length > 0 && context.branches
.filter(branch => !branch.remote)
.map(repo => {
return { value: repo.name, label: repo.name }
})
setLocalBranchOptions(localBranches)
}, [context.branches])
return (
<>
<div className="btn-group w-100" role="group" aria-label="Basic example">
<GitUIButton type="button" onClick={async () => merge()} className="btn btn-primary mr-1">Merge</GitUIButton>
</div>
<label>Merge from Branch</label>
<Select
options={localBranchOptions}
isDisabled={context.branches.length === 0}
onChange={(e: any) => e && onLocalBranchChange(e.value)}
theme={selectTheme}
styles={selectStyles}
isClearable={true}
value={{ value: localBranch, label: localBranch }}
placeholder="Type to search for a branch..."
/>
</>)
}

@ -0,0 +1,196 @@
import React, { useEffect, useState } from "react";
import { gitActionsContext } from "../../../state/context";
import { gitPluginContext } from "../../gitui";
import { selectStyles, selectTheme } from "../../../types/styles";
import Select, { Options, OptionsOrGroups } from 'react-select'
import GitUIButton from "../../buttons/gituibutton";
import { remote } from "../../../types";
import { relative } from "path";
export const PushPull = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [remoteBranch, setRemoteBranch] = useState('')
const [localBranch, setLocalBranch] = useState('')
const [localBranchOptions, setLocalBranchOptions] = useState<any>([]);
const [remoteBranchOptions, setRemoteBranchOptions] = useState<any>([]);
const [localRemotesOptions, setLocalRemotesOptions] = useState<any>([]);
const [disabled, setDisabled] = useState(false)
const [force, setForce] = useState(false)
useEffect(() => {
setRemoteBranch(context.currentBranch.name)
setLocalBranch(context.currentBranch.name)
const currentUpstreamIsInRemotes = context.upstream && context.remotes.find(r => r.name === context.upstream.name)
if (!context.upstream || !currentUpstreamIsInRemotes) {
if (context.currentBranch && context.currentBranch.remote && context.currentBranch.remote.name) {
actions.setUpstreamRemote(context.currentBranch.remote)
setDisabled(false)
} else {
if (context.remotes && context.remotes.length > 0) {
actions.setUpstreamRemote(context.remotes[0])
setDisabled(false)
} else {
actions.setUpstreamRemote(null)
setDisabled(true)
}
}
}
}, [context.currentBranch, context.remotes, context.branches])
const onRemoteBranchChange = (value: string) => {
setRemoteBranch(value)
}
const onLocalBranchChange = (value: any) => {
setLocalBranch(value)
}
const onRemoteChange = (value: string) => {
const remote: remote = context.remotes.find(r => r.name === value)
if (remote) {
actions.setUpstreamRemote(remote)
}
}
const onForceChange = (event: any) => {
const target = event.target;
const value = target.checked;
setForce(value)
}
const push = async () => {
await actions.push({
remote: context.upstream,
ref: {
name: localBranch,
remote: null
},
remoteRef: {
name: remoteBranch,
remote: null
},
force: force
})
await actions.fetch({
remote: context.upstream,
ref: {
name: localBranch,
remote: null
},
remoteRef: {
name: remoteBranch,
remote: null
},
depth: 1,
relative: true,
singleBranch: true
})
}
const pull = async () => {
await actions.pull({
remote: context.upstream,
ref: {
name: localBranch,
remote: null
},
remoteRef: {
name: remoteBranch,
remote: null
},
})
}
useEffect(() => {
const localBranches = context.branches && context.branches.length > 0 && context.branches
.filter(branch => !branch.remote)
.map(repo => {
return { value: repo.name, label: repo.name }
})
setLocalBranchOptions(localBranches)
const remoteBranches = context.branches && context.branches.length > 0 && context.branches
.filter(branch => branch.remote)
.map(repo => {
return { value: repo.name, label: repo.name }
}
)
setRemoteBranchOptions(remoteBranches)
}, [context.branches])
useEffect(() => {
// map context.repositories to options
const options = context.remotes && context.remotes.length > 0 && context.remotes
.map(repo => {
return { value: repo.name, label: repo.name }
})
setLocalRemotesOptions(options)
}, [context.remotes])
const pushPullIsDisabled = () => {
return localBranch === '' || remoteBranch === '' || !context.upstream || context.remotes.length === 0
}
return (
<>
{disabled? <div data-id='disabled' className='text-sm w-100 alert alert-warning mt-1'>
You cannot push or pull because you haven't connected to or selected a remote.
</div>: null}
<div className="btn-group w-100 mt-2" role="group">
<GitUIButton data-id='sourcecontrol-pull' disabledCondition={pushPullIsDisabled()} type="button" onClick={async () => pull()} className="btn btn-primary mr-1">Pull</GitUIButton>
<GitUIButton data-id='sourcecontrol-push' disabledCondition={pushPullIsDisabled()} type="button" onClick={async () => push()} className="btn btn-primary">Push</GitUIButton>
</div>
<label>Local Branch</label>
<Select
id='commands-local-branch-select'
options={localBranchOptions}
isDisabled={context.branches.length === 0}
onChange={(e: any) => e && onLocalBranchChange(e.value)}
theme={selectTheme}
styles={selectStyles}
isClearable={true}
value={{ value: localBranch, label: localBranch }}
placeholder="Type to search for a branch..."
/>
<label>Remote Branch</label>
<Select
id='commands-remote-branch-select'
options={remoteBranchOptions}
isDisabled={context.branches.length === 0}
onChange={(e: any) => e && onRemoteBranchChange(e.value)}
theme={selectTheme}
styles={selectStyles}
isClearable={true}
value={{ value: remoteBranch, label: remoteBranch }}
placeholder="Type to search for a branch..."
/>
<label>Remote</label>
<Select
id='commands-remote-origin-select'
options={localRemotesOptions}
isDisabled={context.remotes.length === 0}
onChange={(e: any) => e && onRemoteChange(e.value)}
theme={selectTheme}
styles={selectStyles}
isClearable={true}
value={{ value: context.upstream && context.upstream.name, label: context.upstream && context.upstream.name }}
placeholder="Type to search for a branch..."
/>
<div className="mt-2 remixui_compilerConfig custom-control custom-checkbox">
<input checked={force} onChange={e => onForceChange(e)} className="remixui_autocompile custom-control-input" type="checkbox" data-id="compilerContainerAutoCompile" id="forcepush" title="Force Push" />
<label className="form-check-label custom-control-label" htmlFor="forcepush">Force push</label>
</div>
</>)
}

@ -0,0 +1,64 @@
import { checkout, ReadCommitResult } from "isomorphic-git";
import React from "react";
import { gitActionsContext } from "../../state/context";
import GitUIButton from "../buttons/gituibutton";
import { gitPluginContext } from "../gitui";
import LoaderIndicator from "../navigation/loaderindicator";
import { BranchDifferences } from "./branches/branchdifferences";
import { CommitDetails } from "./commits/commitdetails";
import { CommitSummary } from "./commits/commitsummary";
export const Commits = () => {
const [hasNextPage, setHasNextPage] = React.useState(true)
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const checkout = async (oid: string) => {
try {
actions.checkout({ ref: oid })
} catch (e) {
//
}
};
const loadNextPage = () => {
actions.fetch({
remote: null,
ref: context.currentBranch,
relative: true,
depth: 5,
singleBranch: true
})
}
const getRemote = () => {
return context.upstream ? context.upstream : context.defaultRemote ? context.defaultRemote : null
}
const getCommitChanges = async (commit: ReadCommitResult) => {
await actions.getCommitChanges(commit.oid, commit.commit.parent[0],null, getRemote())
}
const fetchIsDisabled = () => {
return (!context.upstream)|| context.remotes.length === 0
}
return (
<>
{context.commits && context.commits.length ?
<><BranchDifferences branch={context.currentBranch}></BranchDifferences><div>
<div data-id={`commits-current-branch-${context.currentBranch && context.currentBranch.name}`} className="pt-1">
{context.commits && context.commits.map((commit, index) => {
return (
<CommitDetails branch={context.currentBranch} getCommitChanges={getCommitChanges} key={index} checkout={checkout} commit={commit}></CommitDetails>
);
})}
</div>
</div>
{hasNextPage && <GitUIButton disabledCondition={fetchIsDisabled()} className="mb-1 ml-2 btn btn-sm" onClick={loadNextPage}>Load more</GitUIButton>}
</>
: <div className="text-muted">No commits</div>}
</>
)
}

@ -0,0 +1,60 @@
import { ReadCommitResult } from "isomorphic-git"
import React, { useEffect, useState } from "react";
import { Accordion } from "react-bootstrap";
import { CommitDetailsNavigation } from "../../navigation/commitdetails";
import { gitActionsContext } from "../../../state/context";
import { gitPluginContext } from "../../gitui";
import { CommitDetailsItems } from "./commitdetailsitem";
import { branch, remote } from "@remix-ui/git";
export interface CommitDetailsProps {
commit: ReadCommitResult;
checkout: (oid: string) => void;
getCommitChanges: (commit: ReadCommitResult) => void;
branch: branch
}
export const CommitDetails = (props: CommitDetailsProps) => {
const { commit, checkout, getCommitChanges, branch } = props;
const actions = React.useContext(gitActionsContext)
const context = React.useContext(gitPluginContext)
const [activePanel, setActivePanel] = useState<string>("");
useEffect(() => {
if (activePanel === "0") {
getCommitChanges(commit)
}
}, [activePanel])
const getRemote = (): remote | null => {
return context.upstream ? context.upstream : context.defaultRemote ? context.defaultRemote : null
}
const commitsAhead = (remote: remote) => {
if (!remote) return [];
return context.branchDifferences[`${remote.name}/${branch.name}`]?.uniqueHeadCommits || [];
}
const isAheadOfRepo = () => {
return commitsAhead(getRemote()).findIndex((c) => c.oid === commit.oid) > -1
}
const openFileOnRemote = (file: string, hash: string) => {
if (!getRemote()) return
window.open(`${getRemote() ? `${getRemote().url}/blob/${hash}/${file}` : ""}`, "_blank")
}
return (<Accordion activeKey={activePanel} defaultActiveKey="">
<CommitDetailsNavigation isAheadOfRepo={isAheadOfRepo()} commit={commit} checkout={checkout} eventKey="0" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className="pl-2 border-left ml-1" eventKey="0">
<>
{context.commitChanges && context.commitChanges.filter(
(change) => change.hashModified === commit.oid && change.hashOriginal === commit.commit.parent[0]
).map((change, index) => {
return (<CommitDetailsItems openFileOnRemote={openFileOnRemote} isAheadOfRepo={isAheadOfRepo()} key={index} commitChange={change}></CommitDetailsItems>)
})}
</>
</Accordion.Collapse>
</Accordion>)
}

@ -0,0 +1,51 @@
import { branch, commitChange } from "../../../types";
import React from "react";
import path from "path";
import { gitActionsContext, pluginActionsContext } from "../../../state/context";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGlobe } from "@fortawesome/free-solid-svg-icons";
export interface CCommitDetailsItemsProps {
commitChange: commitChange;
isAheadOfRepo: boolean;
openFileOnRemote: (file: string, hash: string) => void;
}
export const CommitDetailsItems = (props: CCommitDetailsItemsProps) => {
const { commitChange, isAheadOfRepo, openFileOnRemote } = props;
const actions = React.useContext(gitActionsContext)
const pluginActions = React.useContext(pluginActionsContext)
const openChanges = async (change: commitChange) => {
await actions.diff(change)
await pluginActions.openDiff(change)
}
const openRemote = () => {
openFileOnRemote(commitChange.path, commitChange.hashModified)
}
function FunctionStatusIcons() {
const status = commitChange.type
return (<>
{status && status.indexOf("modified") === -1 ? <></> : <span>M</span>}
{status && status.indexOf("deleted") === -1 ? <></> : <span>D</span>}
{status && status.indexOf("added") === -1 ? <></> : <span>A</span>}
</>)
}
return (<>
<div data-id={`commit-change-${commitChange.type}-${path.basename(commitChange.path)}`} className={`d-flex w-100 d-flex flex-row commitdetailsitem ${isAheadOfRepo ? 'text-success' : ''}`}>
<div className='pointer gitfile long-and-truncated' onClick={async () => await openChanges(commitChange)}>
<span className='font-weight-bold long-and-truncated'>{path.basename(commitChange.path)}</span>
<div className='text-secondary long-and-truncated'> {commitChange.path}</div>
</div>
<div className="d-flex align-items-end">
{!isAheadOfRepo ?
<FontAwesomeIcon role={'button'} icon={faGlobe} onClick={() => openRemote()} className="pointer mr-1 align-self-center" /> : <></>}
<FunctionStatusIcons></FunctionStatusIcons>
</div>
</div>
</>)
}

@ -0,0 +1,70 @@
import { ReadCommitResult } from "isomorphic-git"
import { default as dateFormat } from "dateformat";
import React from "react";
import { faGlobe } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { remote } from "@remix-ui/git";
import GitUIButton from "../../buttons/gituibutton";
import { gitPluginContext } from "../../gitui";
export interface CommitSummaryProps {
commit: ReadCommitResult;
checkout: (oid: string) => void;
isAheadOfRepo: boolean
}
export const CommitSummary = (props: CommitSummaryProps) => {
const { commit, checkout, isAheadOfRepo } = props;
const context = React.useContext(gitPluginContext)
const getDate = (commit: ReadCommitResult) => {
const timestamp = commit.commit.author.timestamp;
if (timestamp) {
// calculate the difference between the current time and the commit time in days or hours or minutes
const diff = Math.floor((Date.now() - timestamp * 1000) / 1000 / 60 / 60 / 24);
if (diff == 0) {
return "today at " + dateFormat(timestamp * 1000, "HH:MM");
} else
if (diff < 1) {
// return how many hours ago
return `${Math.floor(diff * 24)} hour(s) ago`;
}
if (diff < 7) {
// return how many days ago
return `${diff} day(s) ago`;
}
if (diff < 365) {
return dateFormat(timestamp * 1000, "mmm dd");
}
return dateFormat(timestamp * 1000, "mmm dd yyyy");
}
return "";
};
const getRemote = (): remote | null => {
return context.upstream ? context.upstream : context.defaultRemote ? context.defaultRemote : null
}
const openRemote = () => {
if (getRemote())
window.open(`${getRemote().url}/commit/${commit.oid}`, '_blank');
}
function removeLineBreaks(str: string): string {
return str.replace(/(\r\n|\n|\r)/gm, '');
}
return (
<>
<div data-id={`commit-summary-${removeLineBreaks(commit.commit.message)}-${isAheadOfRepo ? 'ahead' : ''}`} className="long-and-truncated ml-2">
{commit.commit.message}
</div>
{commit.commit.author.name || ""}
<span className="ml-1">{getDate(commit)}</span>
{getRemote() && getRemote()?.url && !isAheadOfRepo && <GitUIButton className="btn btn-sm p-0 text-muted ml-1" onClick={() => openRemote()}><FontAwesomeIcon icon={faGlobe} ></FontAwesomeIcon></GitUIButton>}
</>
)
}

@ -0,0 +1,90 @@
import React, { useEffect } from "react";
import { gitActionsContext, pluginActionsContext } from "../../state/context";
import { gitPluginContext, loaderContext } from "../gitui";
import { CustomTooltip } from "@remix-ui/helper";
import { useIntl, FormattedMessage } from "react-intl";
import { CopyToClipboard } from "@remix-ui/clipboard";
export const GitHubCredentials = () => {
const context = React.useContext(gitPluginContext)
const pluginactions = React.useContext(pluginActionsContext)
const loader = React.useContext(loaderContext)
const actions = React.useContext(gitActionsContext)
const [githubToken, setGithubToken] = React.useState('')
const [githubUsername, setGithubUsername] = React.useState('')
const [githubEmail, setGithubEmail] = React.useState('')
const [scopeWarning, setScopeWarning] = React.useState(false)
const intl = useIntl()
useEffect(() => {
refresh()
if (context.gitHubUser){
setScopeWarning(!(context.gitHubScopes && context.gitHubScopes.length > 0))
} else {
setScopeWarning(false)
}
}, [loader.plugin, context.gitHubAccessToken, context.userEmails, context.gitHubUser, context.gitHubScopes])
function handleChangeTokenState(e: string): void {
setGithubToken(e)
}
function handleChangeUserNameState(e: string): void {
setGithubUsername(e)
}
function handleChangeEmailState(e: string): void {
setGithubEmail(e)
}
async function saveGithubToken() {
await pluginactions.saveGitHubCredentials({
username: githubUsername,
email: githubEmail,
token: githubToken
})
}
async function refresh() {
const credentials = await pluginactions.getGitHubCredentialsFromLocalStorage()
if (!credentials) return
setGithubToken(credentials.token || '')
setGithubUsername(credentials.username || '')
setGithubEmail(credentials.email || '')
}
function removeToken(): void {
setGithubToken('')
setGithubUsername('')
setGithubEmail('')
pluginactions.saveGitHubCredentials({
username: '',
email: '',
token: ''
})
}
return (
<>
<div className="input-group text-secondary mb-1 h6">
<input data-id='githubToken' type="password" value={githubToken} placeholder="GitHub token" className="form-control" name='githubToken' onChange={e => handleChangeTokenState(e.target.value)} />
<div className="input-group-append">
<CopyToClipboard content={githubToken} data-id='copyToClipboardCopyIcon' className='far fa-copy ml-1 p-2 mt-1' direction={"top"} />
</div>
</div>
<input data-id='gitubUsername' name='githubUsername' onChange={e => handleChangeUserNameState(e.target.value)} value={githubUsername} className="form-control mb-1" placeholder="Git username" type="text" id="githubUsername" />
<input data-id='githubEmail' name='githubEmail' onChange={e => handleChangeEmailState(e.target.value)} value={githubEmail} className="form-control mb-1" placeholder="Git email" type="text" id="githubEmail" />
<div className="d-flex justify-content-between">
<button data-id='saveGitHubCredentials' className="btn btn-primary w-100" onClick={saveGithubToken}>
<FormattedMessage id="save" defaultMessage="Save" />
</button>
<button className="btn btn-danger far fa-trash-alt" onClick={removeToken}>
</button>
</div>
{scopeWarning?
<div className="text-warning">Your GitHub token may not have the correct permissions. Please use the login with GitHub feature.</div>:null}
<hr />
</>
);
}

@ -0,0 +1,29 @@
import { CustomTooltip } from '@remix-ui/helper';
import { pull } from 'lodash';
import React, { useContext } from 'react';
import { FormattedMessage } from 'react-intl';
import { gitActionsContext } from '../../state/context';
import GitUIButton from '../buttons/gituibutton';
export const Init = () => {
const actions = React.useContext(gitActionsContext)
const init = async () => {
actions.init()
}
return (
<>
<div>
<div className='mt-1 mb-2'>
<GitUIButton
onClick={init}
className="btn w-md-25 w-100 btn-primary"
data-id="initgit-btn"
><FormattedMessage id='git.init'/></GitUIButton>
</div>
</div>
</>
)
}

@ -0,0 +1,39 @@
// src/LogViewer.tsx
import React, { useContext } from 'react';
import { gitPluginContext } from '../gitui';
const LogViewer = () => {
const context = useContext(gitPluginContext);
const typeToCssClass = (type: string) => {
switch (type) {
case 'error':
return 'text-danger';
case 'warning':
return 'text-warning';
case 'info':
return 'text-info';
case 'debug':
return 'text-secondary';
default:
return 'text-success';
}
};
if (context.log && context.log.length > 0) {
return (
<div className="p-1">
{context.log && context.log.reverse().map((log, index) => (
<div key={index} className={`log-entry ${typeToCssClass(log.type)}`}>
[{log.type.toUpperCase()}] {log.message}
</div>
))}
</div>
);
} else {
return <div className="p-1">No logs</div>
}
};
export default LogViewer;

@ -0,0 +1,53 @@
import React, { useEffect } from "react";
import { gitActionsContext } from "../../state/context";
import { gitPluginContext } from "../gitui";
import { Remoteselect } from "./remoteselect";
import { RemotesImport } from "./remotesimport";
export const Remotes = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [remoteName, setRemoteName] = React.useState<string>('')
const [url, setUrl] = React.useState<string>('')
const onRemoteNameChange = (value: string) => {
setRemoteName(value)
}
const onUrlChange = (value: string) => {
setUrl(value)
}
const addRemote = async () => {
actions.addRemote({
name: remoteName,
url: url
})
}
return (
<>
<div data-id="remotes-panel-content">
{context.remotes && context.remotes.length ?
<>
{context.remotes && context.remotes.map((remote, index) => {
return (
<Remoteselect key={index} remote={remote}></Remoteselect>
);
})}
</> : <>No remotes</>}
<hr></hr>
<input placeholder="remote name" name='remotename' onChange={e => onRemoteNameChange(e.target.value)} value={remoteName} className="form-control mb-2" type="text" id="remotename" />
<input placeholder="remote url" name='remoteurl' onChange={e => onUrlChange(e.target.value)} value={url} className="form-control" type="text" id="remoteurl" />
<button disabled={(remoteName && url) ? false : true} className='btn btn-primary mt-1 w-100' onClick={async () => {
addRemote();
}}>add remote</button>
<hr />
<RemotesImport />
<hr />
</div>
</>)
}

@ -0,0 +1,37 @@
import { branch, checkout, ReadCommitResult } from "isomorphic-git";
import React, { useEffect, useState } from "react";
import { gitActionsContext } from "../../state/context";
import { gitPluginContext } from "../gitui";
import { default as dateFormat } from "dateformat";
import { RemotesDetailsNavigation } from "../navigation/remotesdetails";
import { Accordion } from "react-bootstrap";
import { remote } from "../../types";
import { RemoteBranchDetails } from "./branches/remotebranchedetails";
export interface RemoteSelectProps {
remote: remote
}
export const Remoteselect = (props: RemoteSelectProps) => {
const { remote } = props;
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [activePanel, setActivePanel] = useState<string>("");
return (
<>
<Accordion activeKey={activePanel} defaultActiveKey="">
<RemotesDetailsNavigation callback={setActivePanel} eventKey="0" activePanel={activePanel} remote={remote} />
<Accordion.Collapse className="pl-2 border-left ml-1" eventKey="0">
<>
{context.branches && context.branches.filter((branch, index) => branch.remote && branch.remote.name === remote.name ).map((branch, index) => {
return (
<RemoteBranchDetails key={index} branch={branch}></RemoteBranchDetails>
);
})}</>
</Accordion.Collapse>
</Accordion>
</>
)
}

@ -0,0 +1,82 @@
import React, { useEffect, useState } from "react";
import { Alert, Button } from "react-bootstrap";
import { gitActionsContext } from "../../state/context";
import { repository } from "../../types";
import { gitPluginContext } from "../gitui";
import Select from 'react-select'
import { selectStyles, selectTheme } from "../../types/styles";
import { TokenWarning } from "./tokenWarning";
import RepositorySelect from "../github/repositoryselect";
export const RemotesImport = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [repo, setRepo] = useState<repository>(null);
const [repoOtions, setRepoOptions] = useState<any>([]);
const [loading, setLoading] = useState(false)
const [show, setShow] = useState(false)
const [remoteName, setRemoteName] = useState('')
useEffect(() => {
if (context.repositories && context.repositories.length > 0) {
// map context.repositories to options
const options = context.repositories && context.repositories.length > 0 && context.repositories.map(repo => {
return { value: repo.id, label: repo.full_name }
})
setRepoOptions(options)
} else {
setRepoOptions(null)
setShow(false)
}
setLoading(false)
}, [context.repositories])
const fetchRepositories = async () => {
try {
setShow(true)
setLoading(true)
setRepoOptions([])
await actions.repositories()
} catch (e) {
// do nothing
}
};
const selectRepo = async (repo: repository) => {
setRepo(repo)
}
const addRemote = async () => {
try {
actions.addRemote({
name: remoteName,
url: repo.html_url
})
} catch (e) {
// do nothing
}
};
const onRemoteNameChange = (value: string) => {
setRemoteName(value)
}
return (
<>
<TokenWarning />
<RepositorySelect select={selectRepo} />
{repo ?
<input data-id='remote-panel-remotename' placeholder="remote name" name='remotename' onChange={e => onRemoteNameChange(e.target.value)} value={remoteName} className="form-control mb-2" type="text" id="remotename" />
: null}
{repo && remoteName ?
<button data-id='remote-panel-addremote' className='btn btn-primary mt-1 w-100' onClick={async () => {
await addRemote()
}}>add {remoteName}:{repo.full_name}</button> : null}
</>
)
}

@ -0,0 +1,48 @@
import { checkout, clone, ReadCommitResult } from "isomorphic-git";
import React from "react";
import { gitActionsContext } from "../../state/context";
import { gitPluginContext } from "../gitui";
import { CustomTooltip } from "@remix-ui/helper";
import { useIntl, FormattedMessage } from "react-intl";
import { CopyToClipboard } from "@remix-ui/clipboard";
import { FormControl, InputGroup } from "react-bootstrap";
export const Settings = () => {
const [githubToken, setGithubToken] = React.useState('')
const [githubUsername, setGithubUsername] = React.useState('')
const [githubEmail, setGithubEmail] = React.useState('')
const intl = useIntl()
const gitAccessTokenLink = 'https://github.com/settings/tokens/new?scopes=gist,repo&description=Remix%20IDE%20Token'
function handleChangeTokenState(e: string): void {
throw new Error("Function not implemented.");
}
function handleChangeUserNameState(e: string): void {
throw new Error("Function not implemented.");
}
function handleChangeEmailState(e: string): void {
throw new Error("Function not implemented.");
}
function saveGithubToken(): void {
throw new Error("Function not implemented.");
}
function removeToken(): void {
throw new Error("Function not implemented.");
}
return (
<>
<input name='githubToken' onChange={e => handleChangeUserNameState(e.target.value)} value={githubToken} className="form-control mb-2" placeholder="GitHub token" type="text" id="githubToken" />
<input name='githubUsername' onChange={e => handleChangeUserNameState(e.target.value)} value={githubUsername} className="form-control mb-2" placeholder="GitHub username" type="text" id="githubUsername" />
<input name='githubEmail' onChange={e => handleChangeEmailState(e.target.value)} value={githubEmail} className="form-control mb-1" placeholder="GitHub email" type="text" id="githubEmail" />
<hr />
</>
);
}

@ -0,0 +1,36 @@
import React, { useEffect, useState } from 'react'
import { GetDeviceCode } from '../github/devicecode'
import { GitHubCredentials } from './githubcredentials'
export const Setup = () => {
const [screen, setScreen] = useState(0)
if (screen === 0) {
return (
<>
<h5>SETUP</h5>
<div>
<div className='mt-1 mb-2'>
To ensure that your commits are properly attributed in Git, you need to configure a username and email address.
These will be used to identify the author of the commit.
</div>
<GetDeviceCode></GetDeviceCode>
<hr></hr>
<GitHubCredentials></GitHubCredentials>
</div>
</>
)
} else if (screen === 1) {
return (
<>
<h5>SETUP</h5>
<h6>Step 2</h6>
<div>
To ensure that your commits are properly attributed in Git, you need to configure your username and email address.
<GitHubCredentials></GitHubCredentials>
</div>
</>
)
}
}

@ -0,0 +1,38 @@
import { ReadCommitResult } from "isomorphic-git"
import React, { useEffect, useState } from "react";
import { Accordion } from "react-bootstrap";
import { gitActionsContext } from "../../../state/context";
import { gitPluginContext } from "../../gitui";
import { sourceControlGroup } from "../../../types";
import { SourceControlGroupNavigation } from "../../navigation/sourcecontrolgroup";
import { SourceControlItem } from "./sourcecontrolitem";
export interface SourceControGroupProps {
group: sourceControlGroup
}
export const SourceControGroup = (props: SourceControGroupProps) => {
const { group } = props;
const actions = React.useContext(gitActionsContext)
const context = React.useContext(gitPluginContext)
const [activePanel, setActivePanel] = useState<string>("0");
useEffect(() => {
if (activePanel === "0") {
}
}, [activePanel])
return (<>
{group.group.length > 0 ?
<Accordion activeKey={activePanel} defaultActiveKey="">
<SourceControlGroupNavigation group={group} eventKey="0" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className="pl-2 border-left ml-1" eventKey="0">
<>
{group.group.map((file, index) => {
return (<SourceControlItem key={index} group={group} file={file}></SourceControlItem>)
})}
</>
</Accordion.Collapse>
</Accordion> : <></>}
</>)
}

@ -0,0 +1,65 @@
import { commitChange, fileStatusResult, sourceControlGroup } from "../../../types";
import React from "react";
import path from "path";
import { gitActionsContext, pluginActionsContext } from "../../../state/context";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGlobe } from "@fortawesome/free-solid-svg-icons";
import { SourceControlItemButtons } from "./sourcontrolitembuttons";
import { removeSlash } from "../../../utils";
export interface SourceControlItemProps {
file: fileStatusResult;
group: sourceControlGroup;
}
export const SourceControlItem = (props: SourceControlItemProps) => {
const { file, group } = props;
const actions = React.useContext(gitActionsContext)
const pluginActions = React.useContext(pluginActionsContext)
async function fileClick(file: fileStatusResult) {
if (file.statusNames && file.statusNames.indexOf("modified") !== -1) {
const headHash = await actions.resolveRef("HEAD")
const change: commitChange = {
path: removeSlash(file.filename),
type: "modified",
hashOriginal: headHash,
hashModified: '',
readonly: false,
}
await actions.diff(change)
await pluginActions.openDiff(change)
} else {
await pluginActions.openFile(file.filename)
//await client.call('fileManager', 'open', file.filename)
}
}
function FunctionStatusIcons() {
const status = file.statusNames
return (<>
{status && status.indexOf("modified") === -1 ? <></> : <div>M</div>}
{status && status.indexOf("deleted") === -1 ? <></> : <span>D</span>}
{status && status.indexOf("added") === -1 ? <></> : <span>A</span>}
{status && status.indexOf("untracked") === -1 ? <></> : <span>U</span>}
</>)
}
if (!file.statusNames || file.statusNames.length === 0) return null
return (<>
<div data-status={file.statusNames.join('-')} data-file={file.filename} className="d-flex w-100 d-flex flex-row align-items-center">
<div className='pointer gitfile long-and-truncated' onClick={async () => await fileClick(file)}>
<span className='font-weight-bold long-and-truncated'>{path.basename(file.filename)}</span>
<div className='text-secondary long-and-truncated'> {file.filename}</div>
</div>
<div className="d-flex align-items-center ml-1">
<SourceControlItemButtons group={group} file={file}></SourceControlItemButtons>
<FunctionStatusIcons></FunctionStatusIcons>
</div>
</div>
</>)
}

@ -0,0 +1,76 @@
import { commitChange, fileStatusResult, sourceControlGroup } from "../../../types";
import React from "react";
import path from "path";
import { gitActionsContext, pluginActionsContext } from "../../../state/context";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGlobe, faMinus, faPlus, faUndo } from "@fortawesome/free-solid-svg-icons";
export interface SourceControlItemButtonsProps {
file: fileStatusResult,
group: sourceControlGroup
}
export const SourceControlItemButtons = (props: SourceControlItemButtonsProps) => {
const { file, group } = props;
const actions = React.useContext(gitActionsContext)
const pluginActions = React.useContext(pluginActionsContext)
function RenderButtons() {
const status = file.statusNames
if (group.name === 'Staged') {
return <>
{status && status.indexOf("modified") === -1 ? <></> :
<button
data-id={`unDo${group.name}${path.basename(file.filename)}`}
onClick={async () => await actions.checkoutfile(file.filename)} className='btn btn-sm btn-secondary mr-1 '>
<FontAwesomeIcon icon={faUndo} className="" /></button>
}
{status && status.indexOf("deleted") === -1 ? <></> :
<button
data-id={`unDo${group.name}${path.basename(file.filename)}`}
onClick={async () => {
await actions.checkoutfile(file.filename)
await actions.add({ filepath: file.filename })
}} className='btn btn-sm btn-secondary mr-1 '>
<FontAwesomeIcon icon={faUndo} className="" /></button>
}
{status && status.indexOf("deleted") !== -1 ? <></> :
<button data-id={`unStage${group.name}${path.basename(file.filename)}`}
onClick={async () => await actions.rm({ filepath: file.filename })} className='btn btn-sm btn-secondary mr-1 '>
<FontAwesomeIcon icon={faMinus} className="" /></button>
}
</>
}
if (group.name === 'Changes') {
return <>
{status && status.indexOf("deleted") === -1 ? <></> :
<><button onClick={async () => await actions.checkoutfile(file.filename)} data-id={`undo${group.name}${path.basename(file.filename)}`} className='btn btn-sm btn-secondary mr-1 '>
<FontAwesomeIcon icon={faUndo} className="" />
</button><button data-id={`addToGit${group.name}${path.basename(file.filename)}`} onClick={async () => await actions.rm({ filepath: file.filename })} className='btn btn-sm btn-secondary mr-1 '>
<FontAwesomeIcon icon={faPlus} className="" /></button></>
}
{status && status.indexOf("modified") === -1 ? <></> :
<button onClick={async () => await actions.checkoutfile(file.filename)} data-id={`undo${group.name}${path.basename(file.filename)}`} className='btn btn-sm btn-secondary mr-1 '>
<FontAwesomeIcon icon={faUndo} className="" /></button>
}
{(status && status.indexOf("unstaged") !== -1 || status && status.indexOf("deleted") !== -1) ? <></> :
<button data-id={`addToGit${group.name}${path.basename(file.filename)}`} onClick={async () => await actions.add({ filepath: file.filename })} className='btn btn-sm btn-secondary mr-1 '>
<FontAwesomeIcon icon={faPlus} className="" /></button>
}
{(status && status.indexOf("unstaged") !== -1 && status && status.indexOf("modified") !== -1) ?
<button data-id={`addToGit${group.name}${path.basename(file.filename)}`} onClick={async () => await actions.add({ filepath: file.filename })} className='btn btn-sm btn-secondary mr-1 '>
<FontAwesomeIcon icon={faPlus} className="" /></button> :
<></>
}
</>
}
return <></>
}
return <RenderButtons />
}

@ -0,0 +1,49 @@
import React, { useEffect, useState } from 'react'
import { gitActionsContext, pluginActionsContext } from '../../state/context'
import { gitPluginContext } from '../gitui'
import { sourceControlGroup } from '../../types'
import { SourceControGroup } from './sourcecontrol/sourcecontrolgroup'
export const SourceControl = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const pluginactions = React.useContext(pluginActionsContext)
const [show, setShow] = useState(false)
useEffect(() => {
if (context.fileStatusResult) {
const total = context.allchangesnotstaged.length
const badges = total + context.staged.length
pluginactions.statusChanged(badges)
setShow((context.deleted.length > 0 || context.staged.length > 0 || context.untracked.length > 0 || context.modified.length > 0))
}
}, [context.fileStatusResult, context.modified, context.allchangesnotstaged, context.untracked, context.deleted])
function RenderGroups() {
const groups: sourceControlGroup[] = [{ name: 'Staged', group: context.staged }, { name: 'Changes', group: context.allchangesnotstaged }]
return (<>
{
groups.map((ob: sourceControlGroup, index: number) => {
return (
<SourceControGroup key={index} group={ob}></SourceControGroup>
)
})
}
</>)
}
return (
<>
{show ?
<>
<div>
<RenderGroups></RenderGroups>
</div></>
: <>
</>}
</>
);
}

@ -0,0 +1,12 @@
import { gitPluginContext } from "../gitui"
import React, { useEffect, useState } from "react";
export const TokenWarning = () => {
const context = React.useContext(gitPluginContext)
return (<>
{(context.gitHubUser && context.gitHubUser.login) ? null :
<li className="text-warning list-group-item d-flex justify-content-between align-items-center">
To use add a GitHub token to the settings.</li>
}
</>
)
}

@ -0,0 +1,37 @@
import { useState } from "react"
export function useLocalStorage(key: string, initialValue: any) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key)
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue
} catch (error) {
// If error also return initialValue
console.error(error)
return initialValue
}
})
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value: any) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value
// Save state
setStoredValue(valueToStore)
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
// A more advanced implementation would handle the error case
console.error(error)
}
}
return [storedValue, setValue]
}

@ -0,0 +1,4 @@
export * from './types'
export { GitUI } from './components/gitui'
export { commitChange, commitChangeType, remote, branch } from './types'
export * from './types/styles'

@ -0,0 +1,56 @@
import { fileStatusResult, gitState } from "../types";
export const getFilesCountByStatus = (status: string, files: fileStatusResult[]) => {
let count = 0;
files.map((m) => {
if (m.statusNames !== undefined) {
if (m.statusNames && m.statusNames.indexOf(status) > -1) {
count++;
}
}
});
return count;
}
export const getFilesByStatus = (status: string, files: fileStatusResult[]): fileStatusResult[] => {
const result: fileStatusResult[] = []
files.map((m) => {
if (m.statusNames !== undefined) {
if (m.statusNames && m.statusNames.indexOf(status) > -1) {
result.push(m)
}
}
});
return result;
}
export const getFilesWithNotModifiedStatus = (files: fileStatusResult[]): fileStatusResult[] => {
const result: fileStatusResult[] = []
files.map((m) => {
if (m.statusNames !== undefined) {
if (m.statusNames && m.statusNames.indexOf("unmodified") === -1) {
result.push(m)
}
}
});
return result;
}
export const allChangedButNotStagedFiles = (files: fileStatusResult[]): fileStatusResult[] => {
let allfiles = getFilesWithNotModifiedStatus(files)
const staged = getFilesByStatus("staged", files)
allfiles = allfiles.filter((trackedFile) => {
return staged.findIndex((stagedFile) => stagedFile.filename === trackedFile.filename) === -1
})
return allfiles;
}
export const getFileStatusForFile = (filename: string, files: fileStatusResult[]) => {
for (let i: number = 0; i < files.length; i++) {
if (files[i].filename === filename)
return files[i].statusNames;
}
}

@ -0,0 +1,852 @@
import { ReadBlobResult, ReadCommitResult } from "isomorphic-git";
import React from "react";
import { fileStatus, fileStatusMerge, setRemoteBranchCommits, resetRemoteBranchCommits, setBranches, setCanCommit, setCommitChanges, setCommits, setCurrentBranch, setGitHubUser, setLoading, setRemoteBranches, setRemotes, setRepos, setUpstream, setLocalBranchCommits, setBranchDifferences, setRemoteAsDefault, setScopes, setLog, clearLog, setUserEmails, setCurrenHead } from "../state/gitpayload";
import { GitHubUser, branch, commitChange, gitActionDispatch, statusMatrixType, gitState, branchDifference, remote, gitLog, fileStatusResult, customGitApi, IGitApi, cloneInputType, fetchInputType, pullInputType, pushInputType, checkoutInput, rmInput, addInput, repository, userEmails } from '../types';
import { removeSlash } from "../utils";
import { disableCallBacks, enableCallBacks } from "./listeners";
import { ModalTypes } from "@remix-ui/app";
import { setFileDecorators } from "./pluginActions";
import { Plugin } from "@remixproject/engine";
import { CustomRemixApi } from "@remix-api";
import { file } from "jszip";
export const fileStatuses = [
["new,untracked", 0, 2, 0], // new, untracked
["added,staged", 0, 2, 2], //
["added,staged,with unstaged changes", 0, 2, 3], // added, staged, with unstaged changes
["unmodified", 1, 1, 1], // unmodified
["modified,unstaged", 1, 2, 1], // modified, unstaged
["modified,staged", 1, 2, 2], // modified, staged
["modified,staged,with unstaged changes", 1, 2, 3], // modified, staged, with unstaged changes
["deleted,unstaged", 1, 0, 1], // deleted, unstaged
["deleted,staged", 1, 0, 0],
["unmodified", 1, 1, 3],
["deleted,not in git", 0, 0, 3],
["unstaged,modified", 1, 2, 0]
];
const statusmatrix: statusMatrixType[] = fileStatuses.map((x: any) => {
return {
matrix: x.shift().split(","),
status: x,
};
});
let plugin: Plugin<any, CustomRemixApi>, dispatch: React.Dispatch<gitActionDispatch>
export const setPlugin = (p: Plugin, dispatcher: React.Dispatch<gitActionDispatch>) => {
plugin = p
dispatch = dispatcher
}
export const init = async () => {
await plugin.call('dgitApi', "init");
await gitlog();
await getBranches();
}
export const getBranches = async () => {
const branches = await plugin.call('dgitApi', "branches")
dispatch(setBranches(branches));
}
export const getRemotes = async () => {
const remotes: remote[] = await plugin.call('dgitApi', "remotes");
dispatch(setRemotes(remotes));
}
export const setUpstreamRemote = async (remote: remote) => {
dispatch(setUpstream(remote));
}
export const getFileStatusMatrix = async (filepaths: string[]) => {
dispatch(setLoading(true))
const fileStatusResult = await statusMatrix(filepaths);
fileStatusResult.map((m) => {
statusmatrix.map((sm) => {
if (JSON.stringify(sm.status) === JSON.stringify(m.status)) {
m.statusNames = sm.matrix;
}
});
});
if (!filepaths) {
dispatch(fileStatus(fileStatusResult))
} else {
dispatch(fileStatusMerge(fileStatusResult))
setFileDecorators(fileStatusResult)
}
dispatch(setLoading(false))
}
export const getCommits = async () => {
try {
const commits: ReadCommitResult[] = await plugin.call(
'dgitApi',
"log",
{ ref: "HEAD" }
);
return commits;
} catch (e) {
return [];
}
}
export const gitlog = async () => {
dispatch(setLoading(true))
let commits = []
try {
commits = await getCommits()
} catch (e) {
}
dispatch(setCommits(commits))
await showCurrentBranch()
dispatch(setLoading(false))
}
export const showCurrentBranch = async () => {
try {
const branch = await currentBranch();
dispatch(setCanCommit((branch && branch.name !== "")));
dispatch(setCurrentBranch(branch));
} catch (e) {
console.log(e)
dispatch(setCanCommit(false));
dispatch(setCurrentBranch({ name: "", remote: { name: "", url: "" } }));
}
try {
const currentHead = await getCommitFromRef('HEAD');
dispatch(setCurrenHead(currentHead));
} catch (e) {
console.log(e)
dispatch(setCurrenHead(''));
}
}
export const currentBranch = async () => {
const branch: branch =
(await plugin.call('dgitApi', "currentbranch")) || {
name: "",
remote: {
name: "",
url: "",
},
};
return branch;
}
export const createBranch = async (name: string = "") => {
dispatch(setLoading(true))
if (name) {
await plugin.call('dgitApi', 'branch', { ref: name, force: true, checkout: true });
}
dispatch(setLoading(false))
}
export const getCommitFromRef = async (ref: string) => {
const commitOid = await plugin.call('dgitApi', "resolveref", {
ref: ref,
});
return commitOid;
}
const settingsWarning = async () => {
const username = await plugin.call('config', 'getAppParameter', 'settings/github-user-name')
const email = await plugin.call('config', 'getAppParameter', 'settings/github-email')
if (!username || !email) {
plugin.call('notification', 'toast', 'Please set your github username and email in the settings')
return false;
} else {
return {
username,
email
};
}
}
export const commit = async (message: string = "") => {
try {
const credentials = await settingsWarning()
if (!credentials) {
dispatch(setLoading(false))
return
}
const sha = await plugin.call('dgitApi', 'commit', {
author: {
name: credentials.username,
email: credentials.email,
},
message: message,
});
sendToGitLog({
type: 'success',
message: `Commited: ${sha}`
})
} catch (err) {
plugin.call('notification', 'toast', `${err}`)
}
}
export const addall = async (files: fileStatusResult[]) => {
try {
const filesToAdd = files
.filter(f => !f.statusNames.includes('deleted'))
.map(f => removeSlash(f.filename))
const filesToRemove = files
.filter(f => f.statusNames.includes('deleted'))
.map(f => removeSlash(f.filename))
try {
add({ filepath: filesToAdd })
} catch (e) { }
sendToGitLog({
type: 'success',
message: `Added all files to git`
})
try {
filesToRemove.map(f => rm({ filepath: f }))
} catch (e) { }
} catch (e) {
plugin.call('notification', 'toast', `${e}`)
}
}
export const add = async (filepath: addInput) => {
try {
if (typeof filepath.filepath === "string") {
filepath.filepath = removeSlash(filepath.filepath)
}
await plugin.call('dgitApi', 'add', filepath);
sendToGitLog({
type: 'success',
message: `Added to git`
})
} catch (e) {
plugin.call('notification', 'toast', `${e}`)
}
}
const getLastCommmit = async () => {
try {
let currentcommitoid = "";
currentcommitoid = await getCommitFromRef("HEAD");
return currentcommitoid;
} catch (e) {
return false;
}
}
export const rm = async (args: rmInput) => {
await plugin.call('dgitApi', 'rm', {
filepath: removeSlash(args.filepath),
});
sendToGitLog({
type: 'success',
message: `Removed from git`
})
}
export const checkoutfile = async (filename: string) => {
const oid = await getLastCommmit();
if (oid)
try {
const commitOid = await plugin.call('dgitApi', 'resolveref', {
ref: oid,
});
const { blob } = await plugin.call('dgitApi', 'readblob', {
oid: commitOid,
filepath: removeSlash(filename),
});
const original = Buffer.from(blob).toString("utf8");
//(original, filename);
await disableCallBacks();
await plugin.call(
"fileManager",
"setFile",
removeSlash(filename),
original
);
await enableCallBacks();
} catch (e) {
plugin.call('notification', 'toast', `No such file`)
}
}
export const checkout = async (cmd: checkoutInput) => {
await disableCallBacks();
await plugin.call('fileManager', 'closeAllFiles')
try {
await plugin.call('dgitApi', 'checkout', cmd)
} catch (e) {
console.log(e)
plugin.call('notification', 'toast', `${e}`)
}
await enableCallBacks();
}
export const clone = async (input: cloneInputType) => {
dispatch(setLoading(true))
const urlParts = input.url.split("/");
const lastPart = urlParts[urlParts.length - 1];
const repoName = lastPart.split(".")[0];
const timestamp = new Date().getTime();
const repoNameWithTimestamp = `${repoName}-${timestamp}`;
try {
await disableCallBacks()
const token = await plugin.call('config' as any, 'getAppParameter' as any, 'settings/gist-access-token')
await plugin.call('dgitApi', 'clone', { ...input, workspaceName: repoNameWithTimestamp });
await enableCallBacks()
sendToGitLog({
type: 'success',
message: `Cloned ${input.url} to ${repoNameWithTimestamp}`
})
} catch (e: any) {
await parseError(e)
}
dispatch(setLoading(false))
}
export const fetch = async (input: fetchInputType) => {
dispatch(setLoading(true))
await disableCallBacks()
try {
await plugin.call('dgitApi', 'fetch', input);
if (!input.quiet) {
await gitlog()
await getBranches()
}
} catch (e: any) {
console.log(e)
await parseError(e)
}
dispatch(setLoading(false))
await enableCallBacks()
}
export const pull = async (input: pullInputType) => {
dispatch(setLoading(true))
await disableCallBacks()
try {
await plugin.call('dgitApi', 'pull', input)
await gitlog()
} catch (e: any) {
console.log(e)
await parseError(e)
}
dispatch(setLoading(false))
await enableCallBacks()
}
export const push = async (input: pushInputType) => {
dispatch(setLoading(true))
await disableCallBacks()
try {
await plugin.call('dgitApi', 'push', input)
} catch (e: any) {
console.log(e)
await parseError(e)
}
dispatch(setLoading(false))
await enableCallBacks()
}
const tokenWarning = async () => {
const token = await plugin.call('config' as any, 'getAppParameter' as any, 'settings/gist-access-token')
if (!token) {
return false;
} else {
return token;
}
}
const parseError = async (e: any) => {
console.trace(e)
// if message conttains 401 Unauthorized, show token warning
if (e.message.includes('401')) {
const result = await plugin.call('notification', 'modal' as any, {
title: 'The GitHub token may be missing or invalid',
message: 'Please check the GitHub token and try again. Error: 401 Unauthorized',
okLabel: 'Go to settings',
cancelLabel: 'Close',
type: ModalTypes.confirm
})
}
// if message contains 404 Not Found, show repo not found
else if (e.message.includes('404')) {
await plugin.call('notification', 'modal' as any, {
title: 'Repository not found',
message: 'Please check the URL and try again.',
okLabel: 'Go to settings',
cancelLabel: 'Close',
type: ModalTypes.confirm
})
}
// if message contains 403 Forbidden
else if (e.message.includes('403')) {
await plugin.call('notification', 'modal' as any, {
title: 'The GitHub token may be missing or invalid',
message: 'Please check the GitHub token and try again. Error: 403 Forbidden',
okLabel: 'Go to settings',
cancelLabel: 'Close',
type: ModalTypes.confirm
})
} else if (e.toString().includes('NotFoundError') && !e.toString().includes('fetch')) {
await plugin.call('notification', 'modal', {
title: 'Remote branch not found',
message: 'The branch you are trying to fetch does not exist on the remote. If you have forked this branch from another branch, you may need to fetch the original branch first or publish this branch on the remote.',
okLabel: 'OK',
type: ModalTypes.alert
})
} else {
await plugin.call('notification', 'alert' as any, {
title: 'Error',
message: e.message
})
}
}
export const repositories = async () => {
try {
const token = await tokenWarning();
if (token) {
let repos = await plugin.call('dgitApi', 'repositories', { token, per_page: 100 })
dispatch(setRepos(repos))
let page = 2
let hasMoreData = true
const per_page = 100
while (hasMoreData) {
const pagedResponse = await plugin.call('dgitApi', 'repositories', { token, page: page, per_page: per_page })
if (pagedResponse.length < per_page) {
hasMoreData = false
}
repos = [...repos, ...pagedResponse]
dispatch(setRepos(repos))
page++
}
} else {
plugin.call('notification', 'alert', {
id: 'github-token-error',
title: 'Error getting repositories',
message: `Please check your GitHub token in the GitHub settings... cannot connect to GitHub`
})
dispatch(setRepos([]))
}
} catch (e) {
console.log(e)
plugin.call('notification', 'alert', {
id: 'github-token-error',
title: 'Error getting repositories',
message: `${e.message}: Please check your GitHub token in the GitHub settings.`
})
dispatch(setRepos([]))
}
}
export const remoteBranches = async (owner: string, repo: string) => {
try {
const token = await tokenWarning();
if (token) {
let branches = await plugin.call('dgitApi' as any, 'remotebranches', { token, owner, repo, per_page: 100 });
dispatch(setRemoteBranches(branches))
let page = 2
let hasMoreData = true
const per_page = 100
while (hasMoreData) {
const pagedResponse = await plugin.call('dgitApi' as any, 'remotebranches', { token, owner, repo, page: page, per_page: per_page })
if (pagedResponse.length < per_page) {
hasMoreData = false
}
branches = [...branches, ...pagedResponse]
dispatch(setRemoteBranches(branches))
page++
}
} else {
plugin.call('notification', 'alert', {
title: 'Error getting branches',
id: 'github-token-error',
message: `Please check your GitHub token in the GitHub settings. It needs to have access to the branches.`
})
dispatch(setRemoteBranches([]))
}
} catch (e) {
console.log(e)
plugin.call('notification', 'alert', {
title: 'Error',
id: 'github-error',
message: e.message
})
dispatch(setRemoteBranches([]))
}
}
export const remoteCommits = async (url: string, branch: string, length: number) => {
const urlParts = url.split("/");
// check if it's github
if (!urlParts[urlParts.length - 3].includes('github')) {
return
}
const owner = urlParts[urlParts.length - 2];
const repo = urlParts[urlParts.length - 1].split(".")[0];
try {
const token = await tokenWarning();
if (token) {
const commits = await plugin.call('dgitApi' as any, 'remotecommits', { token, owner, repo, branch, length });
} else {
sendToGitLog({
type: 'error',
message: `Please check your GitHub token in the GitHub settings.`
})
}
} catch (e) {
// do nothing
}
}
export const saveGitHubCredentials = async (credentials: { username: string, email: string, token: string }) => {
try {
const storedEmail = await plugin.call('config', 'getAppParameter', 'settings/github-email')
const storedUsername = await plugin.call('config', 'getAppParameter', 'settings/github-user-name')
const storedToken = await plugin.call('config', 'getAppParameter', 'settings/gist-access-token')
if (storedUsername !== credentials.username) await plugin.call('config', 'setAppParameter', 'settings/github-user-name', credentials.username)
if (storedEmail !== credentials.email) await plugin.call('config', 'setAppParameter', 'settings/github-email', credentials.email)
if (storedToken !== credentials.token) await plugin.call('config', 'setAppParameter', 'settings/gist-access-token', credentials.token)
const userFetched = await loadGitHubUserFromToken()
if (!userFetched) {
if (credentials.username && credentials.email) {
await plugin.call('notification', 'alert', {
title: 'Error',
id: 'github-credentials-error',
message: `Could not retreive the user from GitHub. You can continue to use the app, but you will not be able to push or pull.`
})
}
dispatch(setGitHubUser({
login: credentials.username,
}))
dispatch(setUserEmails([{
email: credentials.email,
primary: true,
verified: null,
visibility: null
}]))
dispatch(setScopes([]))
}
} catch (e) {
console.log(e)
}
}
export const getGitHubCredentialsFromLocalStorage = async () => {
if (!plugin) return
try {
const username = await plugin.call('config', 'getAppParameter', 'settings/github-user-name')
const email = await plugin.call('config', 'getAppParameter', 'settings/github-email')
const token = await plugin.call('config', 'getAppParameter', 'settings/gist-access-token')
return {
username,
email,
token
}
} catch (e) {
console.log(e)
}
}
export const showAlert = async ({ title, message }: { title: string, message: string }) => {
await plugin.call('notification', 'alert', {
id: 'github-alert',
title: title,
message: message
})
}
export const loadGitHubUserFromToken = async () => {
if (!plugin) return
try {
const token = await tokenWarning();
if (token) {
const data: {
user: GitHubUser,
scopes: string[]
emails: userEmails
} = await plugin.call('dgitApi' as any, 'getGitHubUser', { token });
if (data && data.emails && data.user && data.user.login) {
const primaryEmail = data.emails.find(email => email.primary)
const storedEmail = await plugin.call('config', 'getAppParameter', 'settings/github-email')
if (primaryEmail && storedEmail !== primaryEmail.email) await plugin.call('config', 'setAppParameter', 'settings/github-email', primaryEmail.email)
const storedUsername = await plugin.call('config', 'getAppParameter', 'settings/github-user-name')
if (data.user && data.user.login && (storedUsername !== data.user.login)) await plugin.call('config', 'setAppParameter', 'settings/github-user-name', data.user.login)
dispatch(setGitHubUser(data.user))
dispatch(setScopes(data.scopes))
dispatch(setUserEmails(data.emails))
sendToGitLog({
type: 'success',
message: `Github user loaded...`
})
return true
} else {
sendToGitLog({
type: 'error',
message: `Please check your GitHub token in the GitHub settings.`
})
dispatch(setGitHubUser(null))
return false
}
} else {
sendToGitLog({
type: 'error',
message: `Please check your GitHub token in the GitHub settings.`
})
dispatch(setGitHubUser(null))
return false
}
} catch (e) {
console.log(e)
return false
}
}
export const statusMatrix = async (filepaths: string[]) => {
const matrix = await plugin.call('dgitApi', 'status', { ref: "HEAD", filepaths: filepaths || ['.']});
const result = (matrix || []).map((x) => {
return {
filename: `/${x.shift()}`,
status: x,
statusNames: []
};
});
return result;
}
export const resolveRef = async (ref: string) => {
const oid = await plugin.call('dgitApi', "resolveref", {
ref,
});
return oid;
}
export const diff = async (commitChange: commitChange) => {
if (!commitChange.hashModified) {
const newcontent = await plugin.call(
"fileManager",
"readFile",//
removeSlash(commitChange.path)
);
commitChange.modified = newcontent;
commitChange.readonly = false;
} else {
try {
const modifiedContentReadBlobResult: ReadBlobResult = await plugin.call('dgitApi', "readblob", {
oid: commitChange.hashModified,
filepath: removeSlash(commitChange.path),
});
const modifiedContent = Buffer.from(modifiedContentReadBlobResult.blob).toString("utf8");
commitChange.modified = modifiedContent;
commitChange.readonly = true;
} catch (e) {
commitChange.modified = "";
}
}
try {
const originalContentReadBlobResult: ReadBlobResult = await plugin.call('dgitApi', "readblob", {
oid: commitChange.hashOriginal,
filepath: removeSlash(commitChange.path),
});
const originalContent = Buffer.from(originalContentReadBlobResult.blob).toString("utf8");
commitChange.original = originalContent;
} catch (e) {
commitChange.original = "";
}
}
export const getCommitChanges = async (oid1: string, oid2: string, branch?: branch, remote?: remote) => {
try {
let log
try {
// check if oid2 exists
log = await plugin.call('dgitApi', 'log', {
ref: branch ? branch.name : 'HEAD',
})
} catch (e) {
console.log(e, 'log error')
}
if (log) {
const foundCommit = log.find((commit: ReadCommitResult) => commit.oid === oid2)
if (!foundCommit && remote) {
//await fetch(remote ? remote.name : null, branch ? branch.name : null, null, 5, true, true)
await fetch({
remote: remote,
singleBranch: true,
quiet: true,
relative: true,
depth: 5,
ref: branch,
remoteRef: null
})
}
}
const result: commitChange[] = await plugin.call('dgitApi', 'getCommitChanges', oid1, oid2)
dispatch(setCommitChanges(result))
return result
} catch (e) {
console.log(e)
return false
}
}
async function getRepoDetails(url: string) {
// Extract the owner and repo name from the URL
const pathParts = new URL(url).pathname.split('/').filter(part => part);
if (pathParts.length < 2) {
throw new Error("URL must be in the format 'https://github.com/[owner]/[repository]'");
}
const owner = pathParts[0];
const repo = pathParts[1];
return { owner, repo };
}
export const fetchBranch = async (branch: branch, page: number) => {
if (!branch.remote || !branch.remote.url) return
const token = await tokenWarning();
if (page == 1) {
dispatch(resetRemoteBranchCommits({ branch }))
}
try {
const { owner, repo } = await getRepoDetails(branch.remote.url);
const rc = await plugin.call('dgitApi', 'remotecommits', { token, owner: owner, repo: repo, branch: branch.name, length, page });
dispatch(setRemoteBranchCommits({ branch, commits: rc }))
} catch (e) {
sendToGitLog({
type: 'error',
message: `Error fetching remote commits: ${e.message}`
})
}
return
}
export const getBranchDifferences = async (branch: branch, remote: remote, state: gitState) => {
if (!remote && state) {
if (state.defaultRemote) {
remote = state.defaultRemote
} else {
remote = state.remotes.find((remote: remote) => remote.name === 'origin')
}
if (!remote && state.remotes[0]) {
remote = state.remotes[0]
}
}
if (!remote) return
try {
const branchDifference: branchDifference = await plugin.call('dgitApi', 'compareBranches', {
branch,
remote
})
dispatch(setBranchDifferences(
{
branch,
remote,
branchDifference: branchDifference
}))
} catch (e) {
// do nothing
}
}
export const getBranchCommits = async (branch: branch, page: number) => {
dispatch(setLoading(true))
await disableCallBacks()
try {
if (!branch.remote) {
const commits: ReadCommitResult[] = await plugin.call('dgitApi', 'log', {
ref: branch.name,
})
dispatch(setLocalBranchCommits({ branch, commits }))
} else {
await fetchBranch(branch, page)
}
} catch (e) {
console.trace(e)
await fetchBranch(branch, page)
}
dispatch(setLoading(false))
await enableCallBacks()
}
export const setDefaultRemote = async (remote: remote) => {
dispatch(setRemoteAsDefault(remote))
}
export const addRemote = async (remote: remote) => {
try {
await plugin.call('dgitApi', 'addremote', remote)
await getRemotes()
} catch (e) {
console.log(e)
}
}
export const removeRemote = async (remote: remote) => {
try {
await plugin.call('dgitApi', 'delremote', remote)
await getRemotes()
} catch (e) {
console.log(e)
}
}
export const sendToGitLog = async (message: gitLog) => {
dispatch(setLog(message))
}
export const clearGitLog = async () => {
dispatch(clearLog())
}

@ -0,0 +1,201 @@
import React from "react";
import { setCanUseApp, setLoading, setRepoName, setGItHubToken, setLog, setGitHubUser, setUserEmails } from "../state/gitpayload";
import { gitActionDispatch } from "../types";
import { Plugin } from "@remixproject/engine";
import { getBranches, getFileStatusMatrix, loadGitHubUserFromToken, getRemotes, gitlog, setPlugin } from "./gitactions";
import { Profile } from "@remixproject/plugin-utils";
import { CustomRemixApi } from "@remix-api";
import { statusChanged } from "./pluginActions";
let plugin: Plugin<any, CustomRemixApi>, gitDispatch: React.Dispatch<gitActionDispatch>, loaderDispatch: React.Dispatch<any>, loadFileQueue: AsyncDebouncedQueue
let callBackEnabled: boolean = false
type AsyncCallback = () => Promise<void>;
class AsyncDebouncedQueue {
private queues: Map<AsyncCallback, { timer: any, lastCall: number }>;
constructor(private delay: number = 300) {
this.queues = new Map();
}
enqueue(callback: AsyncCallback, customDelay?:number): void {
if (this.queues.has(callback)) {
clearTimeout(this.queues.get(callback)!.timer);
}
const timer = setTimeout(async () => {
await callback();
this.queues.delete(callback);
}, customDelay || this.delay);
this.queues.set(callback, { timer, lastCall: Date.now() });
}
}
export const setCallBacks = (viewPlugin: Plugin, gitDispatcher: React.Dispatch<gitActionDispatch>, loaderDispatcher: React.Dispatch<any>, setAtivePanel: React.Dispatch<React.SetStateAction<string>>) => {
plugin = viewPlugin
gitDispatch = gitDispatcher
loaderDispatch = loaderDispatcher
loadFileQueue = new AsyncDebouncedQueue()
setPlugin(viewPlugin, gitDispatcher)
plugin.on("fileManager", "fileSaved", async (file: string) => {
loadFileQueue.enqueue(async () => {
loadFiles()
})
});
plugin.on("fileManager", "fileAdded", async (e) => {
loadFileQueue.enqueue(async () => {
loadFiles()
})
});
plugin.on("fileManager", "fileRemoved", async (e) => {
loadFileQueue.enqueue(async () => {
loadFiles()
})
});
plugin.on("fileManager", "fileRenamed", async (oldfile, newfile) => {
loadFileQueue.enqueue(async () => {
loadFiles()
})
});
plugin.on("filePanel", "setWorkspace", async (x: any) => {
gitDispatch(setCanUseApp(x && !x.isLocalhost && x.name))
loadFileQueue.enqueue(async () => {
loadFiles()
})
loadFileQueue.enqueue(async () => {
gitlog()
})
loadFileQueue.enqueue(async () => {
getBranches()
})
loadFileQueue.enqueue(async () => {
getRemotes()
})
});
plugin.on('dgitApi', 'checkout', async () => {
loadFileQueue.enqueue(async () => {
gitlog()
})
loadFileQueue.enqueue(async () => {
getBranches()
})
gitDispatch(setLog({
message: "Checkout",
type: "success"
}))
})
plugin.on('dgitApi', 'init', async () => {
loadFileQueue.enqueue(async () => {
loadFiles()
}, 10)
})
plugin.on('dgitApi', 'add', async () => {
loadFileQueue.enqueue(async () => {
loadFiles()
}, 10)
})
plugin.on('dgitApi', 'rm', async () => {
loadFileQueue.enqueue(async () => {
loadFiles()
}, 10)
})
plugin.on('dgitApi', 'commit', async () => {
loadFileQueue.enqueue(async () => {
gitlog()
}, 10)
loadFileQueue.enqueue(async () => {
getBranches()
}, 20)
gitDispatch(setLog({
message: 'Committed changes...',
type: 'success'
}))
})
plugin.on('dgitApi', 'branch', async () => {
loadFileQueue.enqueue(async () => {
gitlog()
})
loadFileQueue.enqueue(async () => {
getBranches()
})
gitDispatch(setLog({
message: "Created Branch",
type: "success"
}))
})
plugin.on('dgitApi', 'clone', async () => {
gitDispatch(setLog({
message: "Cloned Repository",
type: "success"
}))
loadFileQueue.enqueue(async () => {
loadFiles()
})
})
plugin.on('manager', 'pluginActivated', async (p: Profile<any>) => {
if (p.name === 'dgitApi') {
loadGitHubUserFromToken();
plugin.off('manager', 'pluginActivated');
}
})
plugin.on('config', 'configChanged', async () => {
await getGitConfig()
})
plugin.on('settings', 'configChanged', async () => {
await getGitConfig()
})
plugin.on('dgit' as any, 'openPanel', async (panel: string) => {
const panels = {
'branches': '2'
}
const panelNumber = panels[panel]
setAtivePanel(panelNumber)
})
callBackEnabled = true;
}
export const getGitConfig = async () => {
const username = await plugin.call('settings', 'get', 'settings/github-user-name')
const email = await plugin.call('settings', 'get', 'settings/github-email')
const token = await plugin.call('settings', 'get', 'settings/gist-access-token')
const config = { username, email, token }
loadGitHubUserFromToken()
return
}
export const loadFiles = async (filepaths: string[] = null) => {
try {
const branch = await plugin.call('dgitApi', "currentbranch")
if (branch) {
await getFileStatusMatrix(filepaths);
} else {
await plugin.call('fileDecorator', 'clearFileDecorators')
statusChanged(0)
}
} catch (e) {
console.error(e);
}
}
export const disableCallBacks = async () => {
callBackEnabled = false;
}
export const enableCallBacks = async () => {
callBackEnabled = true;
}

@ -0,0 +1,100 @@
import { commitChange, fileStatusResult, gitActionDispatch, gitState } from "../types"
import { fileDecoration, fileDecorationType } from "@remix-ui/file-decorators"
import { removeSlash } from "../utils"
import { getFilesByStatus } from "./fileHelpers"
import { CustomRemixApi } from "@remix-api";
import { Plugin } from "@remixproject/engine";
let plugin: Plugin<any, CustomRemixApi>, gitDispatch: React.Dispatch<gitActionDispatch>, loaderDispatch: React.Dispatch<any>
export const setPlugin = (p: Plugin<any, CustomRemixApi>, gitDispatcher: React.Dispatch<gitActionDispatch>, loaderDispatcher: React.Dispatch<any>) => {
plugin = p
gitDispatch = gitDispatcher
loaderDispatch = loaderDispatcher
}
export const statusChanged = (badges: number) => {
if (!plugin) return
plugin.emit('statusChanged', {
key: badges === 0 ? 'none' : badges,
type: badges === 0 ? '' : 'success',
title: 'Git changes'
})
}
export const openFile = async (path: string) => {
if (!plugin) return
await plugin.call('fileManager', 'open', path)
}
export const openDiff = async (change: commitChange) => {
if (!plugin) return
plugin.call('fileManager', 'diff', change)
}
export const saveToken = async (token: string) => {
if (!plugin) return
await plugin.call('config', 'setAppParameter', 'settings/gist-access-token', token)
}
export const setFileDecorators = async (files: fileStatusResult[]) => {
if (!plugin) return
const modified = getFilesByStatus('modified', files)
const untracked = getFilesByStatus('untracked', files)
const unmodified = getFilesByStatus('unmodified', files)
await setModifiedDecorator(modified)
await setUntrackedDecorator(untracked)
unmodified.forEach((file) => {
clearFileDecorator(removeSlash(file.filename))
})
}
export const setModifiedDecorator = async (files: fileStatusResult[]) => {
const decorators: fileDecoration[] = []
for (const file of files) {
const decorator: fileDecoration = {
path: removeSlash(file.filename),
isDirectory: false,
fileStateType: fileDecorationType.Custom,
fileStateLabelClass: 'text-warning',
fileStateIconClass: 'text-warning',
fileStateIcon: '',
text: 'M',
owner: 'git',
bubble: true,
comment: 'Modified'
}
decorators.push(decorator)
}
await plugin.call('fileDecorator', 'setFileDecorators', decorators)
}
export const setUntrackedDecorator = async (files: fileStatusResult[]) => {
const decorators: fileDecoration[] = []
for (const file of files) {
const decorator: fileDecoration = {
path: removeSlash(file.filename),
isDirectory: false,
fileStateType: fileDecorationType.Custom,
fileStateLabelClass: 'text-success',
fileStateIconClass: 'text-success',
fileStateIcon: '',
text: 'U',
owner: 'git',
bubble: true,
comment: 'Untracked'
}
decorators.push(decorator)
}
await plugin.call('fileDecorator', 'setFileDecorators', decorators)
}
export const clearFileDecorator = async(path: string) => {
await plugin.call('fileDecorator', 'clearFileDecorators', path)
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save