{
16 | return {
17 | autoClose: false,
18 | actions: [
19 | {
20 | label: trans.__('Show'),
21 | callback: () => {
22 | showErrorMessage(
23 | trans.__('Error'),
24 | {
25 | // Render error in a element to preserve line breaks and
26 | // use a monospace font so e.g. pre-commit errors are readable.
27 | // Ref: https://github.com/jupyterlab/jupyterlab-git/issues/1407
28 | message: (
29 |
30 | {error.message || error.stack || String(error)}
31 |
32 | )
33 | },
34 | [Dialog.warnButton({ label: trans.__('Dismiss') })]
35 | );
36 | },
37 | displayType: 'warn'
38 | } as Notification.IAction
39 | ]
40 | };
41 | }
42 |
43 | /**
44 | * Display additional information in a dialog from a notification
45 | * button.
46 | *
47 | * Note: it will not add a button if the message is empty.
48 | *
49 | * @param message Details to display
50 | * @param trans Translation object
51 | * @returns Notification option to display the message
52 | */
53 | export function showDetails(
54 | message: string,
55 | trans: TranslationBundle
56 | ): Notification.IOptions {
57 | return message
58 | ? {
59 | autoClose: 5000,
60 | actions: [
61 | {
62 | label: trans.__('Details'),
63 | callback: () => {
64 | showErrorMessage(trans.__('Detailed message'), message, [
65 | Dialog.okButton({ label: trans.__('Dismiss') })
66 | ]);
67 | },
68 | displayType: 'warn'
69 | } as Notification.IAction
70 | ]
71 | }
72 | : {};
73 | }
74 |
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
1 | import { URLExt } from '@jupyterlab/coreutils';
2 | import { ServerConnection } from '@jupyterlab/services';
3 | import { Git } from './tokens';
4 | import { requestAPI } from './git';
5 | import { version } from './version';
6 | import { TranslationBundle } from '@jupyterlab/translation';
7 |
8 | /**
9 | * Obtain the server settings or provide meaningful error message for the end user
10 | *
11 | * @returns The server settings
12 | *
13 | * @throws {ServerConnection.ResponseError} If the response was not ok
14 | * @throws {ServerConnection.NetworkError} If the request failed to reach the server
15 | */
16 | export async function getServerSettings(
17 | trans: TranslationBundle
18 | ): Promise {
19 | try {
20 | const endpoint = 'settings' + URLExt.objectToQueryString({ version });
21 | const settings = await requestAPI(endpoint, 'GET');
22 | return settings;
23 | } catch (error) {
24 | if (error instanceof Git.GitResponseError) {
25 | const response = error.response;
26 | if (response.status === 404) {
27 | const message = trans.__(
28 | 'Git server extension is unavailable. Please ensure you have installed the ' +
29 | 'JupyterLab Git server extension by running: pip install --upgrade jupyterlab-git. ' +
30 | 'To confirm that the server extension is installed, run: jupyter server extension list.'
31 | );
32 | throw new ServerConnection.ResponseError(response, message);
33 | } else {
34 | const message = error.message;
35 | console.error('Failed to get the server extension settings', message);
36 | throw new ServerConnection.ResponseError(response, message);
37 | }
38 | } else {
39 | throw error;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/style/ActionButtonStyle.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 | import type { NestedCSSProperties } from 'typestyle/lib/types';
3 |
4 | export const actionButtonStyle = style({
5 | flex: '0 0 auto',
6 | background: 'none',
7 | lineHeight: '0px',
8 | padding: '0px 0px',
9 | width: '16px',
10 | border: 'none',
11 | outline: 'none',
12 | cursor: 'pointer',
13 | margin: '0 4px',
14 |
15 | $nest: {
16 | '&:active': {
17 | transform: 'scale(1.272019649)',
18 | overflow: 'hidden',
19 | backgroundColor: 'var(--jp-layout-color3)'
20 | },
21 |
22 | '&:disabled': {
23 | opacity: 0.4,
24 | background: 'none',
25 | cursor: 'not-allowed'
26 | },
27 |
28 | '&:hover': {
29 | backgroundColor: 'var(--jp-layout-color2)'
30 | }
31 | }
32 | });
33 |
34 | export const hiddenButtonStyle = style({
35 | display: 'none'
36 | });
37 |
38 | export const showButtonOnHover = (() => {
39 | const styled: NestedCSSProperties = {
40 | $nest: {}
41 | };
42 | const selector = `&:hover .${hiddenButtonStyle}`;
43 | styled.$nest![selector] = {
44 | display: 'block'
45 | };
46 | return styled;
47 | })();
48 |
--------------------------------------------------------------------------------
/src/style/BranchMenu.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 | import { showButtonOnHover } from './ActionButtonStyle';
3 |
4 | export const nameClass = style({
5 | flex: '1 1 auto',
6 | textOverflow: 'ellipsis',
7 | overflow: 'hidden',
8 | whiteSpace: 'nowrap'
9 | });
10 |
11 | export const wrapperClass = style({
12 | marginTop: '6px',
13 | marginBottom: '0',
14 |
15 | borderBottom: 'var(--jp-border-width) solid var(--jp-border-color2)'
16 | });
17 |
18 | export const filterWrapperClass = style({
19 | padding: '4px 11px 4px',
20 | display: 'flex'
21 | });
22 |
23 | export const filterClass = style({
24 | flex: '1 1 auto',
25 | boxSizing: 'border-box',
26 | display: 'inline-block',
27 | position: 'relative',
28 | fontSize: 'var(--jp-ui-font-size1)'
29 | });
30 |
31 | export const filterInputClass = style({
32 | boxSizing: 'border-box',
33 |
34 | width: '100%',
35 | height: '2em',
36 |
37 | /* top | right | bottom | left */
38 | padding: '1px 18px 2px 7px',
39 |
40 | color: 'var(--jp-ui-font-color1)',
41 | fontSize: 'var(--jp-ui-font-size1)',
42 | fontWeight: 300,
43 |
44 | backgroundColor: 'var(--jp-layout-color1)',
45 |
46 | border: 'var(--jp-border-width) solid var(--jp-border-color2)',
47 | borderRadius: '3px',
48 |
49 | $nest: {
50 | '&:active': {
51 | border: 'var(--jp-border-width) solid var(--jp-brand-color1)'
52 | },
53 | '&:focus': {
54 | border: 'var(--jp-border-width) solid var(--jp-brand-color1)'
55 | }
56 | }
57 | });
58 |
59 | export const filterClearClass = style({
60 | position: 'absolute',
61 | right: '5px',
62 | top: '0.6em',
63 |
64 | height: '1.1em',
65 | width: '1.1em',
66 |
67 | padding: 0,
68 |
69 | backgroundColor: 'var(--jp-inverse-layout-color4)',
70 |
71 | border: 'none',
72 | borderRadius: '50%',
73 |
74 | $nest: {
75 | svg: {
76 | width: '0.5em!important',
77 | height: '0.5em!important',
78 |
79 | fill: 'var(--jp-ui-inverse-font-color0)'
80 | },
81 | '&:hover': {
82 | backgroundColor: 'var(--jp-inverse-layout-color3)'
83 | },
84 | '&:active': {
85 | backgroundColor: 'var(--jp-inverse-layout-color2)'
86 | }
87 | }
88 | });
89 |
90 | export const newBranchButtonClass = style({
91 | boxSizing: 'border-box',
92 |
93 | width: '7.7em',
94 | height: '2em',
95 | flex: '0 0 auto',
96 |
97 | marginLeft: '5px',
98 |
99 | color: 'white',
100 | fontSize: 'var(--jp-ui-font-size1)',
101 |
102 | backgroundColor: 'var(--md-blue-500)',
103 | border: '0',
104 | borderRadius: '3px',
105 |
106 | $nest: {
107 | '&:hover': {
108 | backgroundColor: 'var(--md-blue-600)'
109 | },
110 | '&:active': {
111 | backgroundColor: 'var(--md-blue-700)'
112 | }
113 | }
114 | });
115 |
116 | export const listItemClass = style(
117 | {
118 | padding: '4px 11px!important',
119 | userSelect: 'none'
120 | },
121 | showButtonOnHover
122 | );
123 |
124 | export const activeListItemClass = style({
125 | color: 'white!important',
126 |
127 | backgroundColor: 'var(--jp-brand-color1)!important',
128 |
129 | $nest: {
130 | '& .jp-icon-selectable[fill]': {
131 | fill: 'white'
132 | }
133 | }
134 | });
135 |
136 | export const listItemIconClass = style({
137 | width: '16px',
138 | height: '16px',
139 |
140 | marginRight: '4px'
141 | });
142 |
--------------------------------------------------------------------------------
/src/style/CommitComparisonBox.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 |
3 | export const commitComparisonBoxStyle = style({
4 | flex: '0 0 auto',
5 | display: 'flex',
6 | flexDirection: 'column',
7 |
8 | marginBlockStart: 0,
9 | marginBlockEnd: 0,
10 | paddingLeft: 0,
11 |
12 | overflowY: 'auto',
13 |
14 | borderTop: 'var(--jp-border-width) solid var(--jp-border-color2)'
15 | });
16 |
17 | export const commitComparisonDiffStyle = style({
18 | paddingLeft: 10
19 | });
20 |
--------------------------------------------------------------------------------
/src/style/FileItemStyle.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 | import type { NestedCSSProperties } from 'typestyle/lib/types';
3 | import { actionButtonStyle, showButtonOnHover } from './ActionButtonStyle';
4 |
5 | export const fileStyle = style(
6 | {
7 | userSelect: 'none',
8 | display: 'flex',
9 | flexDirection: 'row',
10 | alignItems: 'center',
11 | boxSizing: 'border-box',
12 | color: 'var(--jp-ui-font-color1)',
13 | lineHeight: 'var(--jp-private-running-item-height)',
14 | padding: '0px 4px',
15 | listStyleType: 'none',
16 |
17 | $nest: {
18 | '&:hover': {
19 | backgroundColor: 'var(--jp-layout-color2)'
20 | }
21 | }
22 | },
23 | showButtonOnHover
24 | );
25 |
26 | export const selectedFileStyle = style(
27 | (() => {
28 | const styled: NestedCSSProperties = {
29 | color: 'white',
30 | background: 'var(--jp-brand-color1)',
31 |
32 | $nest: {
33 | '&:hover': {
34 | color: 'white',
35 | background: 'var(--jp-brand-color1) !important'
36 | },
37 | '&:hover .jp-icon-selectable[fill]': {
38 | fill: 'white'
39 | },
40 | '&:hover .jp-icon-selectable[stroke]': {
41 | stroke: 'white'
42 | },
43 | '& .jp-icon-selectable[fill]': {
44 | fill: 'white'
45 | },
46 | '& .jp-icon-selectable-inverse[fill]': {
47 | fill: 'var(--jp-brand-color1)'
48 | }
49 | }
50 | };
51 |
52 | styled.$nest![`& .${actionButtonStyle}:active`] = {
53 | backgroundColor: 'var(--jp-brand-color1)'
54 | };
55 |
56 | styled.$nest![`& .${actionButtonStyle}:hover`] = {
57 | backgroundColor: 'var(--jp-brand-color1)'
58 | };
59 |
60 | return styled;
61 | })()
62 | );
63 |
64 | export const fileChangedLabelStyle = style({
65 | fontSize: '10px',
66 | marginLeft: '5px'
67 | });
68 |
69 | export const selectedFileChangedLabelStyle = style({
70 | color: 'white !important'
71 | });
72 |
73 | export const fileChangedLabelBrandStyle = style({
74 | color: 'var(--jp-brand-color0)'
75 | });
76 |
77 | export const fileChangedLabelWarnStyle = style({
78 | color: 'var(--jp-warn-color0)',
79 | fontWeight: 'bold'
80 | });
81 |
82 | export const fileChangedLabelInfoStyle = style({
83 | color: 'var(--jp-info-color0)'
84 | });
85 |
86 | export const fileGitButtonStyle = style({
87 | display: 'none'
88 | });
89 |
90 | export const fileButtonStyle = style({
91 | marginTop: '5px'
92 | });
93 |
94 | export const gitMarkBoxStyle = style({
95 | flex: '0 0 auto'
96 | });
97 |
98 | export const checkboxLabelStyle = style({
99 | display: 'flex',
100 | alignItems: 'center'
101 | });
102 |
103 | export const checkboxLabelContainerStyle = style({
104 | display: 'flex',
105 | width: '100%'
106 | });
107 |
108 | export const checkboxLabelLastContainerStyle = style({
109 | display: 'flex',
110 | marginLeft: 'auto',
111 | overflow: 'hidden'
112 | });
113 |
--------------------------------------------------------------------------------
/src/style/FileListStyle.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 |
3 | export const fileListWrapperClass = style({
4 | flex: '1 1 auto',
5 | minHeight: '150px',
6 |
7 | overflow: 'hidden',
8 | overflowY: 'auto'
9 | });
10 |
--------------------------------------------------------------------------------
/src/style/FilePathStyle.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 |
3 | export const fileIconStyle = style({
4 | flex: '0 0 auto',
5 | height: '16px',
6 | width: '16px',
7 | marginRight: '4px'
8 | });
9 |
10 | export const fileLabelStyle = style({
11 | flex: '1 1 auto',
12 | fontSize: 'var(--jp-ui-font-size1)',
13 | overflow: 'hidden',
14 | textOverflow: 'ellipsis',
15 | whiteSpace: 'nowrap'
16 | });
17 |
18 | export const folderLabelStyle = style({
19 | color: 'var(--jp-ui-font-color2)',
20 | fontSize: 'var(--jp-ui-font-size0)',
21 | margin: '0px 4px'
22 | });
23 |
--------------------------------------------------------------------------------
/src/style/GitPanel.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 |
3 | export const panelWrapperClass = style({
4 | display: 'flex',
5 | flexDirection: 'column',
6 | height: '100%',
7 | overflowY: 'auto'
8 | });
9 |
10 | export const warningTextClass = style({
11 | fontSize: 'var(--jp-ui-font-size1)',
12 | lineHeight: 'var(--jp-content-line-height)',
13 | margin: '13px 11px 4px 11px',
14 | textAlign: 'left'
15 | });
16 |
17 | export const repoButtonClass = style({
18 | alignSelf: 'center',
19 | boxSizing: 'border-box',
20 |
21 | height: '28px',
22 | width: '200px',
23 | marginTop: '5px',
24 | border: '0',
25 | borderRadius: '3px',
26 |
27 | color: 'white',
28 | fontSize: 'var(--jp-ui-font-size1)',
29 |
30 | backgroundColor: 'var(--md-blue-500)',
31 | $nest: {
32 | '&:hover': {
33 | backgroundColor: 'var(--md-blue-600)'
34 | },
35 | '&:active': {
36 | backgroundColor: 'var(--md-blue-700)'
37 | }
38 | }
39 | });
40 |
41 | export const tabsClass = style({
42 | minHeight: '36px!important',
43 |
44 | $nest: {
45 | 'button:last-of-type': {
46 | borderRight: 'none'
47 | }
48 | }
49 | });
50 |
51 | export const tabClass = style({
52 | width: '50%',
53 | minWidth: '0!important',
54 | maxWidth: '50%!important',
55 | minHeight: '36px!important',
56 |
57 | color: 'var(--jp-ui-font-color1)!important',
58 | backgroundColor: 'var(--jp-layout-color2)!important',
59 |
60 | borderBottom:
61 | 'var(--jp-border-width) solid var(--jp-border-color2)!important',
62 | borderRight: 'var(--jp-border-width) solid var(--jp-border-color2)!important',
63 |
64 | // @ts-expect-error unknown value
65 | textTransform: 'none !important'
66 | });
67 |
68 | export const selectedTabClass = style({
69 | backgroundColor: 'var(--jp-layout-color1)!important'
70 | });
71 |
72 | export const tabIndicatorClass = style({
73 | height: '3px!important',
74 |
75 | backgroundColor: 'var(--jp-brand-color1)!important',
76 | transition: 'none!important'
77 | });
78 |
--------------------------------------------------------------------------------
/src/style/GitStageStyle.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 | import type { NestedCSSProperties } from 'typestyle/lib/types';
3 | import { hiddenButtonStyle, showButtonOnHover } from './ActionButtonStyle';
4 |
5 | export const sectionAreaStyle = style(
6 | {
7 | display: 'flex',
8 | flexDirection: 'row',
9 | alignItems: 'center',
10 | padding: '4px',
11 | fontWeight: 600,
12 | borderBottom: 'var(--jp-border-width) solid var(--jp-border-color2)',
13 | letterSpacing: '1px',
14 | fontSize: '12px',
15 | overflowY: 'hidden',
16 | height: '16px',
17 |
18 | $nest: {
19 | '&:hover': {
20 | backgroundColor: 'var(--jp-layout-color2)'
21 | }
22 | }
23 | },
24 | showButtonOnHover
25 | );
26 |
27 | export const sectionFileContainerStyle = style(
28 | (() => {
29 | const styled: NestedCSSProperties = {
30 | margin: '0',
31 | padding: '0',
32 | overflow: 'auto',
33 | $nest: {}
34 | };
35 |
36 | const focus = `&:focus-within .${sectionAreaStyle} .${hiddenButtonStyle}`;
37 | styled.$nest![focus] = {
38 | display: 'block'
39 | };
40 | const hoverSelector = `&:hover .${sectionAreaStyle} .${hiddenButtonStyle}`;
41 | styled.$nest![hoverSelector] = {
42 | display: 'block'
43 | };
44 | return styled;
45 | })()
46 | );
47 |
48 | export const sectionHeaderLabelStyle = style({
49 | fontSize: 'var(--jp-ui-font-size1)',
50 | flex: '1 1 auto',
51 | textOverflow: 'ellipsis',
52 | overflow: 'hidden',
53 | whiteSpace: 'nowrap'
54 | });
55 |
56 | export const sectionHeaderSizeStyle = style({
57 | fontSize: 'var(--jp-ui-font-size1)',
58 | flex: '0 0 auto',
59 | whiteSpace: 'nowrap',
60 | borderRadius: '2px'
61 | });
62 |
63 | export const changeStageButtonStyle = style({
64 | flex: '0 0 auto',
65 | backgroundColor: 'transparent',
66 | height: '13px',
67 | border: 'none',
68 | outline: 'none',
69 | paddingLeft: '0px'
70 | });
71 |
--------------------------------------------------------------------------------
/src/style/GitStashStyle.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 | import type { NestedCSSProperties } from 'typestyle/lib/types';
3 | import { sectionAreaStyle } from './GitStageStyle';
4 |
5 | export const stashContainerStyle = style(
6 | (() => {
7 | const styled: NestedCSSProperties = { $nest: {} };
8 |
9 | styled.$nest![`& > .${sectionAreaStyle}`] = {
10 | margin: 0
11 | };
12 | return styled;
13 | })()
14 | );
15 |
16 | export const sectionHeaderLabelStyle = style({
17 | fontSize: 'var(--jp-ui-font-size1)',
18 | flex: '1 1 auto',
19 | textOverflow: 'ellipsis',
20 | overflow: 'hidden',
21 | whiteSpace: 'nowrap',
22 | display: 'flex',
23 | justifyContent: 'space-between',
24 | alignSelf: 'flex-start'
25 | });
26 |
27 | export const sectionButtonContainerStyle = style({
28 | display: 'flex'
29 | });
30 |
31 | export const stashFileStyle = style({
32 | display: 'flex',
33 | padding: '0 4px'
34 | });
35 |
36 | export const listStyle = style({
37 | overflowX: 'hidden',
38 | $nest: {
39 | '&>*': {
40 | margin: 0,
41 | padding: 0
42 | }
43 | }
44 | });
45 |
46 | export const stashEntryMessageStyle = style({
47 | textOverflow: 'ellipsis',
48 | overflow: 'hidden',
49 | whiteSpace: 'nowrap',
50 | display: 'inline-block'
51 | });
52 |
--------------------------------------------------------------------------------
/src/style/GitWidgetStyle.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 |
3 | export const gitWidgetStyle = style({
4 | display: 'flex',
5 | flexDirection: 'column',
6 | minWidth: '300px',
7 | color: 'var(--jp-ui-font-color1)',
8 | background: 'var(--jp-layout-color1)',
9 | fontSize: 'var(--jp-ui-font-size1)'
10 | });
11 |
--------------------------------------------------------------------------------
/src/style/HistorySideBarStyle.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 |
3 | export const selectedHistoryFileStyle = style({
4 | minHeight: '48px',
5 |
6 | top: 0,
7 | position: 'sticky',
8 |
9 | flexGrow: 0,
10 | flexShrink: 0,
11 |
12 | overflowX: 'hidden',
13 |
14 | backgroundColor: 'var(--jp-toolbar-active-background)'
15 | });
16 |
17 | export const noHistoryFoundStyle = style({
18 | display: 'flex',
19 | justifyContent: 'center',
20 |
21 | padding: '10px 0',
22 |
23 | color: 'var(--jp-ui-font-color2)'
24 | });
25 |
26 | export const historySideBarStyle = style({
27 | flex: '1 1 auto',
28 | display: 'flex',
29 | flexDirection: 'column',
30 |
31 | minHeight: '200px',
32 |
33 | marginBlockStart: 0,
34 | marginBlockEnd: 0,
35 | paddingLeft: 0,
36 | paddingRight: '8px',
37 |
38 | overflowY: 'auto'
39 | });
40 |
41 | export const historySideBarWrapperStyle = style({
42 | display: 'flex'
43 | });
44 |
--------------------------------------------------------------------------------
/src/style/ManageRemoteDialog.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 |
3 | export const remoteDialogClass = style({
4 | color: 'var(--jp-ui-font-color1)!important',
5 |
6 | borderRadius: '3px!important',
7 |
8 | backgroundColor: 'var(--jp-layout-color1)!important'
9 | });
10 |
11 | export const remoteDialogInputClass = style({
12 | display: 'flex',
13 | flexDirection: 'column',
14 | $nest: {
15 | '& > input': {
16 | marginTop: '10px',
17 | lineHeight: '20px'
18 | }
19 | }
20 | });
21 |
22 | export const actionsWrapperClass = style({
23 | padding: '15px 0px !important',
24 | justifyContent: 'space-around !important'
25 | });
26 |
27 | export const existingRemoteWrapperClass = style({
28 | margin: '1.5rem 0rem 1rem',
29 | padding: '0px'
30 | });
31 |
32 | export const existingRemoteGridClass = style({
33 | marginTop: '2px',
34 | display: 'grid',
35 | rowGap: '5px',
36 | columnGap: '10px',
37 | gridTemplateColumns: 'auto auto auto'
38 | });
39 |
--------------------------------------------------------------------------------
/src/style/NewTagDialog.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 |
3 | export const historyDialogBoxStyle = style({
4 | flex: '1 1 auto',
5 | display: 'flex',
6 | flexDirection: 'column',
7 |
8 | minHeight: '200px',
9 |
10 | marginBlockStart: 0,
11 | marginBlockEnd: 0,
12 | paddingLeft: 0,
13 |
14 | listStyleType: 'none'
15 | });
16 |
17 | export const historyDialogBoxWrapperStyle = style({
18 | display: 'flex',
19 | height: '200px',
20 | overflowY: 'auto'
21 | });
22 |
23 | export const activeListItemClass = style({
24 | backgroundColor: 'var(--jp-brand-color1)!important',
25 |
26 | $nest: {
27 | '& .jp-icon-selectable[fill]': {
28 | fill: 'white'
29 | }
30 | }
31 | });
32 |
33 | export const commitHeaderBoldClass = style({
34 | color: 'white!important',
35 | fontWeight: '700'
36 | });
37 |
38 | export const commitItemBoldClass = style({
39 | color: 'white!important'
40 | });
41 |
42 | export const commitWrapperClass = style({
43 | flexGrow: 0,
44 | display: 'flex',
45 | flexShrink: 0,
46 | flexDirection: 'column',
47 | padding: '5px 0px 5px 10px',
48 | borderBottom: 'var(--jp-border-width) solid var(--jp-border-color2)'
49 | });
50 |
51 | export const commitHeaderClass = style({
52 | display: 'flex',
53 | color: 'var(--jp-ui-font-color2)',
54 | paddingBottom: '5px'
55 | });
56 |
57 | export const commitHeaderItemClass = style({
58 | width: '30%',
59 |
60 | paddingLeft: '0.5em',
61 |
62 | overflow: 'hidden',
63 | whiteSpace: 'nowrap',
64 | textOverflow: 'ellipsis',
65 | textAlign: 'left',
66 |
67 | $nest: {
68 | '&:first-child': {
69 | paddingLeft: 0
70 | }
71 | }
72 | });
73 |
74 | export const commitBodyClass = style({
75 | flex: 'auto'
76 | });
77 |
--------------------------------------------------------------------------------
/src/style/PastCommitNode.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 |
3 | export const commitWrapperClass = style({
4 | flexGrow: 0,
5 | display: 'flex',
6 | flexShrink: 0,
7 | flexDirection: 'column',
8 | padding: '5px 0px 5px 10px',
9 | borderBottom: 'var(--jp-border-width) solid var(--jp-border-color2)'
10 | });
11 |
12 | export const commitHeaderClass = style({
13 | display: 'flex',
14 | color: 'var(--jp-ui-font-color2)',
15 | paddingBottom: '5px'
16 | });
17 |
18 | export const commitHeaderItemClass = style({
19 | width: '30%',
20 |
21 | paddingLeft: '0.5em',
22 |
23 | overflow: 'hidden',
24 | whiteSpace: 'nowrap',
25 | textOverflow: 'ellipsis',
26 | textAlign: 'left',
27 |
28 | $nest: {
29 | '&:first-child': {
30 | paddingLeft: 0
31 | }
32 | }
33 | });
34 |
35 | export const branchWrapperClass = style({
36 | display: 'flex',
37 | fontSize: '0.8em',
38 | marginLeft: '-5px'
39 | });
40 |
41 | export const branchClass = style({
42 | padding: '2px',
43 | // Special case, regardless of theme, because
44 | // backgrounds of colors are not based on theme either
45 | color: 'var(--md-grey-900)',
46 | border: 'var(--jp-border-width) solid var(--md-grey-700)',
47 | borderRadius: '4px',
48 | margin: '3px'
49 | });
50 |
51 | export const remoteBranchClass = style({
52 | backgroundColor: 'var(--md-blue-100)'
53 | });
54 |
55 | export const localBranchClass = style({
56 | backgroundColor: 'var(--md-orange-100)'
57 | });
58 |
59 | export const workingBranchClass = style({
60 | backgroundColor: 'var(--md-red-100)'
61 | });
62 |
63 | export const commitExpandedClass = style({
64 | backgroundColor: 'var(--jp-layout-color1)'
65 | });
66 |
67 | export const commitBodyClass = style({
68 | flex: 'auto'
69 | });
70 |
71 | export const iconButtonClass = style({
72 | // width: '16px',
73 | // height: '16px'
74 | });
75 |
76 | export const singleFileCommitClass = style({
77 | $nest: {
78 | '&:hover': {
79 | backgroundColor: 'var(--jp-layout-color2)'
80 | }
81 | }
82 | });
83 |
84 | export const referenceCommitNodeClass = style({
85 | borderLeft: '6px solid var(--jp-git-diff-deleted-color1)'
86 | });
87 |
88 | export const challengerCommitNodeClass = style({
89 | borderLeft: '6px solid var(--jp-git-diff-added-color1)'
90 | });
91 |
--------------------------------------------------------------------------------
/src/style/RebaseActionStyle.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 |
3 | export const rebaseActionStyle = style({
4 | padding: '8px'
5 | });
6 |
--------------------------------------------------------------------------------
/src/style/SinglePastCommitInfo.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 |
3 | export const commitClass = style({
4 | flex: '0 0 auto',
5 | width: '100%',
6 | fontSize: '12px',
7 | marginBottom: '10px',
8 | marginTop: '5px',
9 | paddingTop: '5px'
10 | });
11 |
12 | export const commitOverviewNumbersClass = style({
13 | fontSize: '13px',
14 | fontWeight: 'bold',
15 | paddingTop: '5px',
16 | $nest: {
17 | '& span': {
18 | alignItems: 'center',
19 | display: 'inline-flex',
20 | marginLeft: '5px'
21 | },
22 | '& span:nth-of-type(1)': {
23 | marginLeft: '0px'
24 | }
25 | }
26 | });
27 |
28 | export const commitDetailClass = style({
29 | flex: '1 1 auto',
30 | margin: '0'
31 | });
32 |
33 | export const commitDetailHeaderClass = style({
34 | paddingBottom: '0.5em',
35 | fontSize: '13px',
36 | fontWeight: 'bold'
37 | });
38 |
39 | export const commitDetailFileClass = style({
40 | userSelect: 'none',
41 | display: 'flex',
42 | flexDirection: 'row',
43 | alignItems: 'center',
44 | color: 'var(--jp-ui-font-color1)',
45 | height: 'var(--jp-private-running-item-height)',
46 | lineHeight: 'var(--jp-private-running-item-height)',
47 | whiteSpace: 'nowrap',
48 |
49 | overflow: 'hidden',
50 |
51 | $nest: {
52 | '&:hover': {
53 | backgroundColor: 'var(--jp-layout-color2)'
54 | },
55 | '&:active': {
56 | backgroundColor: 'var(--jp-layout-color3)'
57 | }
58 | }
59 | });
60 |
61 | export const iconClass = style({
62 | display: 'inline-block',
63 | width: '13px',
64 | height: '13px',
65 | right: '10px'
66 | });
67 |
68 | export const insertionsIconClass = style({
69 | $nest: {
70 | '.jp-icon3': {
71 | fill: 'var(--md-green-500)'
72 | }
73 | }
74 | });
75 |
76 | export const deletionsIconClass = style({
77 | $nest: {
78 | '.jp-icon3': {
79 | fill: 'var(--md-red-500)'
80 | }
81 | }
82 | });
83 |
84 | export const fileListClass = style({
85 | $nest: {
86 | ul: {
87 | paddingLeft: 0,
88 | margin: 0
89 | }
90 | }
91 | });
92 |
93 | export const actionButtonClass = style({
94 | float: 'right'
95 | });
96 |
97 | export const commitBodyClass = style({
98 | paddingTop: '5px',
99 | whiteSpace: 'pre-wrap',
100 | wordWrap: 'break-word',
101 | margin: '0'
102 | });
103 |
--------------------------------------------------------------------------------
/src/style/StatusWidget.ts:
--------------------------------------------------------------------------------
1 | import { keyframes, style } from 'typestyle';
2 |
3 | const fillAnimation = keyframes({
4 | to: { fillOpacity: 1 }
5 | });
6 |
7 | export const statusIconClass = style({
8 | $nest: {
9 | '& .jp-icon3': {
10 | animationName: fillAnimation,
11 | animationDuration: '1s'
12 | }
13 | }
14 | });
15 |
16 | const pathAnimation = keyframes({
17 | '0%': { fillOpacity: 1 },
18 | '50%': { fillOpacity: 0.6 },
19 | '100%': { fillOpacity: 1 }
20 | });
21 |
22 | export const statusAnimatedIconClass = style({
23 | $nest: {
24 | '& .jp-icon3': {
25 | animationName: pathAnimation,
26 | animationDuration: '2s',
27 | animationIterationCount: 'infinite'
28 | }
29 | }
30 | });
31 |
32 | export const badgeClass = style({
33 | $nest: {
34 | '& > .MuiBadge-badge': {
35 | top: 6,
36 | right: 15,
37 | backgroundColor: 'var(--jp-warn-color1)'
38 | }
39 | }
40 | });
41 |
42 | export const currentBranchNameClass = style({
43 | fontSize: 'var(--jp-ui-font-size1)',
44 | lineHeight: '100%'
45 | });
46 |
--------------------------------------------------------------------------------
/src/style/SubmoduleMenuStyle.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 |
3 | export const submoduleHeaderStyle = style({
4 | padding: '4px 4px 1px',
5 | margin: '0 6px',
6 | fontWeight: 600,
7 | letterSpacing: '1px',
8 | fontSize: '12px',
9 | overflowY: 'hidden',
10 | borderBottom: '3px solid var(--jp-brand-color1)',
11 | height: '16px'
12 | });
13 |
--------------------------------------------------------------------------------
/src/style/SuspendModal.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 |
3 | export const fullscreenProgressClass = style({
4 | position: 'absolute',
5 | top: '50%',
6 | left: '50%',
7 | color: 'var(--jp-ui-inverse-font-color0)',
8 | textAlign: 'center'
9 | });
10 |
--------------------------------------------------------------------------------
/src/style/Toolbar.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 |
3 | export const toolbarClass = style({
4 | display: 'flex',
5 | flexDirection: 'column',
6 |
7 | backgroundColor: 'var(--jp-layout-color1)'
8 | });
9 |
10 | export const toolbarNavClass = style({
11 | display: 'flex',
12 | flexDirection: 'row',
13 | flexWrap: 'wrap',
14 |
15 | minHeight: '35px',
16 | lineHeight: 'var(--jp-private-running-item-height)',
17 |
18 | backgroundColor: 'var(--jp-layout-color1)',
19 |
20 | borderBottom: 'var(--jp-border-width) solid var(--jp-border-color2)'
21 | });
22 |
23 | export const toolbarMenuWrapperClass = style({
24 | background: 'var(--jp-layout-color1)'
25 | });
26 |
27 | export const toolbarMenuButtonClass = style({
28 | boxSizing: 'border-box',
29 | display: 'flex',
30 | flexDirection: 'row',
31 | flexWrap: 'wrap',
32 |
33 | width: '100%',
34 | minHeight: '50px',
35 |
36 | /* top | right | bottom | left */
37 | padding: '4px 11px 4px 11px',
38 |
39 | fontSize: 'var(--jp-ui-font-size1)',
40 | lineHeight: '1.5em',
41 | color: 'var(--jp-ui-font-color1)',
42 | textAlign: 'left',
43 |
44 | border: 'none',
45 | borderBottom: 'var(--jp-border-width) solid var(--jp-border-color2)',
46 | borderRadius: 0,
47 |
48 | background: 'var(--jp-layout-color1)'
49 | });
50 |
51 | export const toolbarMenuButtonEnabledClass = style({
52 | $nest: {
53 | '&:hover': {
54 | backgroundColor: 'var(--jp-layout-color2)'
55 | },
56 | '&:active': {
57 | backgroundColor: 'var(--jp-layout-color3)'
58 | }
59 | }
60 | });
61 |
62 | export const toolbarMenuButtonIconClass = style({
63 | width: '16px',
64 | height: '16px',
65 |
66 | /* top | right | bottom | left */
67 | margin: 'auto 8px auto 0'
68 | });
69 |
70 | export const toolbarMenuButtonTitleWrapperClass = style({
71 | flexBasis: 0,
72 | flexGrow: 1,
73 |
74 | marginTop: 'auto',
75 | marginBottom: 'auto',
76 | marginRight: 'auto',
77 |
78 | $nest: {
79 | '& > p': {
80 | marginTop: 0,
81 | marginBottom: 0
82 | }
83 | }
84 | });
85 |
86 | export const toolbarMenuButtonTitleClass = style({});
87 |
88 | export const toolbarMenuButtonSubtitleClass = style({
89 | marginBottom: 'auto',
90 |
91 | fontWeight: 700
92 | });
93 |
94 | // Styles overriding default button style are marked as important to ensure application
95 | export const toolbarButtonClass = style({
96 | boxSizing: 'border-box',
97 | height: '24px',
98 | width: 'var(--jp-private-running-button-width) !important',
99 |
100 | margin: 'auto 0 auto 0',
101 | padding: '0px 6px !important',
102 |
103 | $nest: {
104 | '& span': {
105 | // Set icon width and centers it
106 | margin: 'auto',
107 | width: '16px'
108 | }
109 | }
110 | });
111 |
112 | export const spacer = style({
113 | flex: '1 1 auto'
114 | });
115 |
116 | export const badgeClass = style({
117 | $nest: {
118 | '& > .MuiBadge-badge': {
119 | top: 12,
120 | right: 5,
121 | backgroundColor: 'var(--jp-warn-color1)'
122 | }
123 | }
124 | });
125 |
--------------------------------------------------------------------------------
/src/style/common.ts:
--------------------------------------------------------------------------------
1 | import { style } from 'typestyle';
2 |
3 | export const toolbarButtonStyle = style({
4 | width: 'var(--jp-private-running-button-width)',
5 | background: 'var(--jp-layout-color1)',
6 | border: 'none',
7 | backgroundSize: '16px',
8 | backgroundRepeat: 'no-repeat',
9 | backgroundPosition: 'center',
10 | boxSizing: 'border-box',
11 | outline: 'none',
12 | padding: '0px 6px',
13 | margin: 'auto 5px auto 5px',
14 | height: '24px',
15 |
16 | $nest: {
17 | '&:hover': {
18 | backgroundColor: 'var(--jp-layout-color2)'
19 | },
20 | '&:active': {
21 | backgroundColor: 'var(--jp-layout-color3)'
22 | }
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/src/style/icons.ts:
--------------------------------------------------------------------------------
1 | import { LabIcon } from '@jupyterlab/ui-components';
2 |
3 | // icon svg import statements
4 | import addSvg from '../../style/icons/add.svg';
5 | import branchSvg from '../../style/icons/branch.svg';
6 | import clockSvg from '../../style/icons/clock.svg';
7 | import cloneSvg from '../../style/icons/clone.svg';
8 | import compareWithSelectedSvg from '../../style/icons/compare-with-selected.svg';
9 | import deletionsMadeSvg from '../../style/icons/deletions.svg';
10 | import desktopSvg from '../../style/icons/desktop.svg';
11 | import diffSvg from '../../style/icons/diff.svg';
12 | import discardSvg from '../../style/icons/discard.svg';
13 | import gitSvg from '../../style/icons/git.svg';
14 | import insertionsMadeSvg from '../../style/icons/insertions.svg';
15 | import mergeSvg from '../../style/icons/merge.svg';
16 | import openSvg from '../../style/icons/open-file.svg';
17 | import pullSvg from '../../style/icons/pull.svg';
18 | import pushSvg from '../../style/icons/push.svg';
19 | import removeSvg from '../../style/icons/remove.svg';
20 | import rewindSvg from '../../style/icons/rewind.svg';
21 | import selectForCompareSvg from '../../style/icons/select-for-compare.svg';
22 | import tagSvg from '../../style/icons/tag.svg';
23 | import trashSvg from '../../style/icons/trash.svg';
24 | import verticalMoreSvg from '../../style/icons/vertical-more.svg';
25 |
26 | export const gitIcon = new LabIcon({ name: 'git', svgstr: gitSvg });
27 | export const addIcon = new LabIcon({
28 | name: 'git:add',
29 | svgstr: addSvg
30 | });
31 | export const branchIcon = new LabIcon({
32 | name: 'git:branch',
33 | svgstr: branchSvg
34 | });
35 | export const cloneIcon = new LabIcon({
36 | name: 'git:clone',
37 | svgstr: cloneSvg
38 | });
39 | export const compareWithSelectedIcon = new LabIcon({
40 | name: 'git:compare-with-selected',
41 | svgstr: compareWithSelectedSvg
42 | });
43 | export const deletionsMadeIcon = new LabIcon({
44 | name: 'git:deletions',
45 | svgstr: deletionsMadeSvg
46 | });
47 | export const desktopIcon = new LabIcon({
48 | name: 'git:desktop',
49 | svgstr: desktopSvg
50 | });
51 | export const diffIcon = new LabIcon({
52 | name: 'git:diff',
53 | svgstr: diffSvg
54 | });
55 | export const discardIcon = new LabIcon({
56 | name: 'git:discard',
57 | svgstr: discardSvg
58 | });
59 | export const insertionsMadeIcon = new LabIcon({
60 | name: 'git:insertions',
61 | svgstr: insertionsMadeSvg
62 | });
63 | export const historyIcon = new LabIcon({
64 | name: 'git:history',
65 | svgstr: clockSvg
66 | });
67 | export const mergeIcon = new LabIcon({
68 | name: 'git:merge',
69 | svgstr: mergeSvg
70 | });
71 | export const openIcon = new LabIcon({
72 | name: 'git:open-file',
73 | svgstr: openSvg
74 | });
75 | export const pullIcon = new LabIcon({
76 | name: 'git:pull',
77 | svgstr: pullSvg
78 | });
79 | export const pushIcon = new LabIcon({
80 | name: 'git:push',
81 | svgstr: pushSvg
82 | });
83 | export const removeIcon = new LabIcon({
84 | name: 'git:remove',
85 | svgstr: removeSvg
86 | });
87 | export const rewindIcon = new LabIcon({
88 | name: 'git:rewind',
89 | svgstr: rewindSvg
90 | });
91 | export const selectForCompareIcon = new LabIcon({
92 | name: 'git:select-for-compare',
93 | svgstr: selectForCompareSvg
94 | });
95 | export const tagIcon = new LabIcon({
96 | name: 'git:tag',
97 | svgstr: tagSvg
98 | });
99 | export const trashIcon = new LabIcon({
100 | name: 'git:trash',
101 | svgstr: trashSvg
102 | });
103 | export const verticalMoreIcon = new LabIcon({
104 | name: 'git:vertical-more',
105 | svgstr: verticalMoreSvg
106 | });
107 |
--------------------------------------------------------------------------------
/src/svg.d.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Jupyter Development Team.
2 | // Distributed under the terms of the Modified BSD License.
3 |
4 | // including this file in a package allows for the use of import statements
5 | // with svg files. Example: `import xSvg from 'path/xSvg.svg'`
6 |
7 | // for use with raw-loader in Webpack.
8 | // The svg will be imported as a raw string
9 |
10 | declare module '*.svg' {
11 | const value: string;
12 | export default value;
13 | }
14 |
--------------------------------------------------------------------------------
/src/svgPathData.ts:
--------------------------------------------------------------------------------
1 | // a canvas like api for building an svg path data attribute
2 | export class SVGPathData {
3 | constructor() {
4 | this._SVGPath = [];
5 | }
6 | toString(): string {
7 | return this._SVGPath.join(' ');
8 | }
9 | moveTo(x: number, y: number): void {
10 | this._SVGPath.push(`M ${x},${y}`);
11 | }
12 | lineTo(x: number, y: number): void {
13 | this._SVGPath.push(`L ${x},${y}`);
14 | }
15 | closePath(): void {
16 | this._SVGPath.push('Z');
17 | }
18 | bezierCurveTo(
19 | cp1x: number,
20 | cp1y: number,
21 | cp2x: number,
22 | cp2y: number,
23 | x: number,
24 | y: number
25 | ): void {
26 | this._SVGPath.push(`C ${cp1x}, ${cp1y}, ${cp2x}, ${cp2y}, ${x}, ${y}`);
27 | }
28 |
29 | private _SVGPath: string[];
30 | }
31 |
--------------------------------------------------------------------------------
/src/version.ts:
--------------------------------------------------------------------------------
1 | // generated by genversion
2 | export const version = '0.51.2';
3 |
--------------------------------------------------------------------------------
/src/widgets/AuthorBox.ts:
--------------------------------------------------------------------------------
1 | import { Dialog } from '@jupyterlab/apputils';
2 | import { nullTranslator, TranslationBundle } from '@jupyterlab/translation';
3 | import { Widget } from '@lumino/widgets';
4 | import { Git } from '../tokens';
5 |
6 | /**
7 | * The UI for the commit author form
8 | */
9 | export class GitAuthorForm
10 | extends Widget
11 | implements Dialog.IBodyWidget
12 | {
13 | constructor({
14 | author,
15 | trans
16 | }: {
17 | author: Git.IIdentity;
18 | trans: TranslationBundle;
19 | }) {
20 | super();
21 | this._populateForm(author, trans);
22 | }
23 |
24 | private _populateForm(
25 | author: Git.IIdentity,
26 | trans?: TranslationBundle
27 | ): void {
28 | trans ??= nullTranslator.load('jupyterlab_git');
29 | const nameLabel = document.createElement('label');
30 | nameLabel.textContent = trans.__('Committer name:');
31 | const emailLabel = document.createElement('label');
32 | emailLabel.textContent = trans.__('Committer email:');
33 |
34 | this._name = nameLabel.appendChild(document.createElement('input'));
35 | this._email = emailLabel.appendChild(document.createElement('input'));
36 | this._name.placeholder = 'Name';
37 | this._email.type = 'text';
38 | this._email.placeholder = 'Email';
39 | this._email.type = 'email';
40 | this._name.value = author.name;
41 | this._email.value = author.email;
42 |
43 | this.node.appendChild(nameLabel);
44 | this.node.appendChild(emailLabel);
45 | }
46 |
47 | /**
48 | * Returns the input value.
49 | */
50 | getValue(): Git.IIdentity {
51 | const credentials = {
52 | name: this._name.value,
53 | email: this._email.value
54 | };
55 | return credentials;
56 | }
57 |
58 | // @ts-expect-error initialization is indirect
59 | private _name: HTMLInputElement;
60 | // @ts-expect-error initialization is indirect
61 | private _email: HTMLInputElement;
62 | }
63 |
--------------------------------------------------------------------------------
/src/widgets/CredentialsBox.tsx:
--------------------------------------------------------------------------------
1 | import { Dialog } from '@jupyterlab/apputils';
2 | import { TranslationBundle } from '@jupyterlab/translation';
3 | import { Widget } from '@lumino/widgets';
4 | import { Git } from '../tokens';
5 |
6 | /**
7 | * The UI for the credentials form
8 | */
9 | export class GitCredentialsForm
10 | extends Widget
11 | implements Dialog.IBodyWidget
12 | {
13 | private _passwordPlaceholder: string;
14 | constructor(
15 | trans: TranslationBundle,
16 | textContent = trans.__('Enter credentials for remote repository'),
17 | warningContent = '',
18 | passwordPlaceholder = trans.__('password / personal access token')
19 | ) {
20 | super();
21 | this._trans = trans;
22 | this._passwordPlaceholder = passwordPlaceholder;
23 | this.node.appendChild(this.createBody(textContent, warningContent));
24 | }
25 |
26 | private createBody(textContent: string, warningContent: string): HTMLElement {
27 | const node = document.createElement('div');
28 | const label = document.createElement('label');
29 |
30 | const checkboxLabel = document.createElement('label');
31 | this._checkboxCacheCredentials = document.createElement('input');
32 | const checkboxText = document.createElement('span');
33 |
34 | this._user = document.createElement('input');
35 | this._user.type = 'text';
36 | this._password = document.createElement('input');
37 | this._password.type = 'password';
38 |
39 | const text = document.createElement('span');
40 | const warning = document.createElement('div');
41 |
42 | node.className = 'jp-CredentialsBox';
43 | warning.className = 'jp-CredentialsBox-warning';
44 | text.textContent = textContent;
45 | warning.textContent = warningContent;
46 | this._user.placeholder = this._trans.__('username');
47 | this._password.placeholder = this._passwordPlaceholder;
48 |
49 | checkboxLabel.className = 'jp-CredentialsBox-label-checkbox';
50 | this._checkboxCacheCredentials.type = 'checkbox';
51 | checkboxText.textContent = this._trans.__('Save my login temporarily');
52 |
53 | label.appendChild(text);
54 | label.appendChild(this._user);
55 | label.appendChild(this._password);
56 | node.appendChild(label);
57 | node.appendChild(warning);
58 |
59 | checkboxLabel.appendChild(this._checkboxCacheCredentials);
60 | checkboxLabel.appendChild(checkboxText);
61 | node.appendChild(checkboxLabel);
62 |
63 | return node;
64 | }
65 |
66 | /**
67 | * Returns the input value.
68 | */
69 | getValue(): Git.IAuth {
70 | return {
71 | username: this._user.value,
72 | password: this._password.value,
73 | cache_credentials: this._checkboxCacheCredentials.checked
74 | };
75 | }
76 | protected _trans: TranslationBundle;
77 | // @ts-expect-error initialization is indirect
78 | private _user: HTMLInputElement;
79 | // @ts-expect-error initialization is indirect
80 | private _password: HTMLInputElement;
81 | // @ts-expect-error initialization is indirect
82 | private _checkboxCacheCredentials: HTMLInputElement;
83 | }
84 |
--------------------------------------------------------------------------------
/src/widgets/GitCloneForm.ts:
--------------------------------------------------------------------------------
1 | import { TranslationBundle } from '@jupyterlab/translation';
2 | import { Widget } from '@lumino/widgets';
3 |
4 | /**
5 | * The UI for the form fields shown within the Clone modal.
6 | */
7 | export class GitCloneForm extends Widget {
8 | /**
9 | * Create a redirect form.
10 | * @param translator - The language translator
11 | */
12 | constructor(trans: TranslationBundle) {
13 | super({ node: GitCloneForm.createFormNode(trans) });
14 | }
15 |
16 | /**
17 | * Returns the input value.
18 | */
19 | getValue(): { url: string; versioning: boolean; submodules: boolean } {
20 | return {
21 | url: encodeURIComponent(
22 | (
23 | this.node.querySelector('#input-link') as HTMLInputElement
24 | ).value.trim()
25 | ),
26 | versioning: Boolean(
27 | encodeURIComponent(
28 | (this.node.querySelector('#download') as HTMLInputElement).checked
29 | )
30 | ),
31 | submodules: Boolean(
32 | encodeURIComponent(
33 | (this.node.querySelector('#submodules') as HTMLInputElement).checked
34 | )
35 | )
36 | };
37 | }
38 |
39 | private static createFormNode(trans: TranslationBundle): HTMLElement {
40 | const node = document.createElement('div');
41 | const inputWrapper = document.createElement('div');
42 | const inputLinkLabel = document.createElement('label');
43 | const inputLink = document.createElement('input');
44 | const linkText = document.createElement('span');
45 | const checkboxWrapper = document.createElement('div');
46 | const submodulesLabel = document.createElement('label');
47 | const submodules = document.createElement('input');
48 | const downloadLabel = document.createElement('label');
49 | const download = document.createElement('input');
50 |
51 | node.className = 'jp-CredentialsBox';
52 | inputWrapper.className = 'jp-RedirectForm';
53 | checkboxWrapper.className = 'jp-CredentialsBox-wrapper';
54 | submodulesLabel.className = 'jp-CredentialsBox-label-checkbox';
55 | downloadLabel.className = 'jp-CredentialsBox-label-checkbox';
56 | submodules.id = 'submodules';
57 | download.id = 'download';
58 | inputLink.id = 'input-link';
59 |
60 | linkText.textContent = trans.__(
61 | 'Enter the URI of the remote Git repository'
62 | );
63 | inputLink.placeholder = 'https://host.com/org/repo.git';
64 |
65 | submodulesLabel.textContent = trans.__('Include submodules');
66 | submodulesLabel.title = trans.__(
67 | 'If checked, the remote submodules in the repository will be cloned recursively'
68 | );
69 | submodules.setAttribute('type', 'checkbox');
70 | submodules.setAttribute('checked', 'checked');
71 |
72 | downloadLabel.textContent = trans.__('Download the repository');
73 | downloadLabel.title = trans.__(
74 | 'If checked, the remote repository default branch will be downloaded instead of cloned'
75 | );
76 | download.setAttribute('type', 'checkbox');
77 |
78 | inputLinkLabel.appendChild(linkText);
79 | inputLinkLabel.appendChild(inputLink);
80 |
81 | inputWrapper.append(inputLinkLabel);
82 |
83 | submodulesLabel.prepend(submodules);
84 | checkboxWrapper.appendChild(submodulesLabel);
85 |
86 | downloadLabel.prepend(download);
87 | checkboxWrapper.appendChild(downloadLabel);
88 |
89 | node.appendChild(inputWrapper);
90 | node.appendChild(checkboxWrapper);
91 |
92 | return node;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/widgets/GitResetToRemoteForm.tsx:
--------------------------------------------------------------------------------
1 | import { Dialog } from '@jupyterlab/apputils';
2 | import { Widget } from '@lumino/widgets';
3 | import { Git } from '../tokens';
4 |
5 | /**
6 | * A widget form containing a text block and a checkbox,
7 | * can be used as a Dialog body.
8 | */
9 | export class CheckboxForm
10 | extends Widget
11 | implements Dialog.IBodyWidget
12 | {
13 | constructor(textBody: string, checkboxLabel: string) {
14 | super();
15 | this.node.appendChild(this.createBody(textBody, checkboxLabel));
16 | }
17 |
18 | private createBody(textBody: string, checkboxLabel: string): HTMLElement {
19 | const mainNode = document.createElement('div');
20 |
21 | const text = document.createElement('div');
22 | text.textContent = textBody;
23 |
24 | const checkboxContainer = document.createElement('label');
25 |
26 | this._checkbox = document.createElement('input');
27 | this._checkbox.type = 'checkbox';
28 | this._checkbox.checked = true;
29 |
30 | const label = document.createElement('span');
31 | label.textContent = checkboxLabel;
32 |
33 | checkboxContainer.appendChild(this._checkbox);
34 | checkboxContainer.appendChild(label);
35 |
36 | mainNode.appendChild(text);
37 | mainNode.appendChild(checkboxContainer);
38 |
39 | return mainNode;
40 | }
41 |
42 | getValue(): Git.ICheckboxFormValue {
43 | return {
44 | checked: this._checkbox.checked
45 | };
46 | }
47 |
48 | // @ts-expect-error initialization is indirect
49 | private _checkbox: HTMLInputElement;
50 | }
51 |
--------------------------------------------------------------------------------
/src/widgets/GitWidget.tsx:
--------------------------------------------------------------------------------
1 | import { ReactWidget } from '@jupyterlab/apputils';
2 | import { FileBrowserModel } from '@jupyterlab/filebrowser';
3 | import { ISettingRegistry } from '@jupyterlab/settingregistry';
4 | import { TranslationBundle } from '@jupyterlab/translation';
5 | import { CommandRegistry } from '@lumino/commands';
6 | import { Message } from '@lumino/messaging';
7 | import { Widget } from '@lumino/widgets';
8 | import * as React from 'react';
9 | import { GitPanel } from '../components/GitPanel';
10 | import { GitExtension } from '../model';
11 | import { gitWidgetStyle } from '../style/GitWidgetStyle';
12 |
13 | /**
14 | * A class that exposes the git plugin Widget.
15 | */
16 | export class GitWidget extends ReactWidget {
17 | constructor(
18 | model: GitExtension,
19 | settings: ISettingRegistry.ISettings,
20 | commands: CommandRegistry,
21 | fileBrowserModel: FileBrowserModel,
22 | trans: TranslationBundle,
23 | options?: Widget.IOptions
24 | ) {
25 | super();
26 | this.node.id = 'GitSession-root';
27 | this.addClass(gitWidgetStyle);
28 |
29 | this._trans = trans;
30 | this._commands = commands;
31 | this._fileBrowserModel = fileBrowserModel;
32 | this._model = model;
33 | this._settings = settings;
34 |
35 | // Add refresh standby condition if this widget is hidden
36 | model.refreshStandbyCondition = (): boolean =>
37 | !this._settings.composite['refreshIfHidden'] && this.isHidden;
38 | }
39 |
40 | /**
41 | * A message handler invoked on a `'before-show'` message.
42 | *
43 | * #### Notes
44 | * The default implementation of this handler is a no-op.
45 | */
46 | onBeforeShow(msg: Message): void {
47 | // Trigger refresh when the widget is displayed
48 | this._model.refresh().catch(error => {
49 | console.error('Fail to refresh model when displaying GitWidget.', error);
50 | });
51 | super.onBeforeShow(msg);
52 | }
53 |
54 | /**
55 | * Render the content of this widget using the virtual DOM.
56 | *
57 | * This method will be called anytime the widget needs to be rendered, which
58 | * includes layout triggered rendering.
59 | */
60 | render(): JSX.Element {
61 | return (
62 |
69 | );
70 | }
71 |
72 | private _commands: CommandRegistry;
73 | private _fileBrowserModel: FileBrowserModel;
74 | private _model: GitExtension;
75 | private _settings: ISettingRegistry.ISettings;
76 | private _trans: TranslationBundle;
77 | }
78 |
--------------------------------------------------------------------------------
/src/widgets/discardAllChanges.ts:
--------------------------------------------------------------------------------
1 | import { showDialog, Dialog, showErrorMessage } from '@jupyterlab/apputils';
2 | import { TranslationBundle } from '@jupyterlab/translation';
3 | import { IGitExtension } from '../tokens';
4 |
5 | /**
6 | * Discard changes in all unstaged and staged files
7 | *
8 | * @param isFallback If dialog is called when the classical pull operation fails
9 | */
10 | export async function discardAllChanges(
11 | model: IGitExtension,
12 | trans: TranslationBundle,
13 | isFallback?: boolean
14 | ): Promise {
15 | const result = await showDialog({
16 | title: trans.__('Discard all changes'),
17 | body: isFallback
18 | ? trans.__(
19 | 'Your current changes forbid pulling the latest changes. Do you want to permanently discard those changes? This action cannot be undone.'
20 | )
21 | : trans.__(
22 | 'Are you sure you want to permanently discard changes to all files? This action cannot be undone.'
23 | ),
24 | buttons: [
25 | Dialog.cancelButton({ label: trans.__('Cancel') }),
26 | Dialog.warnButton({ label: trans.__('Discard') })
27 | ]
28 | });
29 |
30 | if (result.button.accept) {
31 | try {
32 | return model.resetToCommit('HEAD');
33 | } catch (reason: any) {
34 | showErrorMessage(trans.__('Discard all changes failed.'), reason);
35 | return Promise.reject(reason);
36 | }
37 | }
38 |
39 | return Promise.reject({
40 | cancelled: true,
41 | message: 'The user refused to discard all changes'
42 | });
43 | }
44 |
--------------------------------------------------------------------------------
/style/advanced-push-form.css:
--------------------------------------------------------------------------------
1 | .jp-remote-text {
2 | font-size: 1rem;
3 | }
4 |
5 | .jp-remote-options-wrapper {
6 | margin: 4px;
7 | display: flex;
8 | flex-direction: column;
9 | align-items: stretch;
10 | row-gap: 5px;
11 | }
12 |
13 | .jp-button-wrapper {
14 | display: flex;
15 | gap: 0.5rem;
16 | align-items: center;
17 | }
18 |
19 | .jp-option {
20 | height: fit-content !important;
21 | appearance: auto !important;
22 | margin: 0;
23 | }
24 |
25 | .jp-force-box-container {
26 | margin-top: 1rem;
27 | display: flex;
28 | align-items: flex-end;
29 | column-gap: 5px;
30 | }
31 |
--------------------------------------------------------------------------------
/style/base.css:
--------------------------------------------------------------------------------
1 | /* -----------------------------------------------------------------------------
2 | | Copyright (c) Jupyter Development Team.
3 | | Distributed under the terms of the Modified BSD License.
4 | |---------------------------------------------------------------------------- */
5 |
6 | @import url('variables.css');
7 | @import url('credentials-box.css');
8 | @import url('diff-common.css');
9 | @import url('status-widget.css');
10 | @import url('advanced-push-form.css');
11 |
12 | .jp-git-tab-mod-preview {
13 | font-style: italic;
14 | }
15 |
16 | .not-saved > .lm-TabBar-tabCloseIcon > :not(:hover) > .jp-icon-busy[fill] {
17 | fill: var(--jp-inverse-layout-color3);
18 | }
19 |
20 | .not-saved > .lm-TabBar-tabCloseIcon > :not(:hover) > .jp-icon3[fill] {
21 | fill: none;
22 | }
23 |
--------------------------------------------------------------------------------
/style/credentials-box.css:
--------------------------------------------------------------------------------
1 | .jp-CredentialsBox input[type='text'],
2 | .jp-CredentialsBox input[type='password'] {
3 | display: block;
4 | width: 100%;
5 | margin-top: 10px;
6 | margin-bottom: 10px;
7 | }
8 |
9 | .jp-CredentialsBox input[type='checkbox'] {
10 | display: inline-block;
11 | }
12 |
13 | .jp-CredentialsBox-warning {
14 | color: var(--jp-warn-color0);
15 | }
16 |
17 | .jp-CredentialsBox-label-checkbox {
18 | display: flex;
19 | align-items: center;
20 | }
21 |
22 | .jp-CredentialsBox-wrapper {
23 | margin-top: 10px;
24 | }
25 |
--------------------------------------------------------------------------------
/style/icons/add.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/style/icons/branch.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/style/icons/clock.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/style/icons/clone.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/style/icons/compare-with-selected.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/style/icons/deletions.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/style/icons/desktop.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/style/icons/diff.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/style/icons/discard.svg:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/style/icons/git.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/style/icons/insertions.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/style/icons/merge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/style/icons/open-file.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/style/icons/plus.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/style/icons/pull.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/style/icons/push.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/style/icons/remove.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/style/icons/rewind.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/style/icons/select-for-compare.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/style/icons/tag.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/style/icons/trash.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/style/icons/vertical-more.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/style/index.css:
--------------------------------------------------------------------------------
1 | /* Import same style from nbdime and nbdime-jupyterlab (see index.ts) */
2 | @import url('~nbdime/lib/common/collapsible.css');
3 | @import url('~nbdime/lib/upstreaming/flexpanel.css');
4 | @import url('~nbdime/lib/common/dragpanel.css');
5 | @import url('~nbdime/lib/styles/variables.css');
6 | @import url('~nbdime/lib/styles/common.css');
7 | @import url('~nbdime/lib/styles/diff.css');
8 | @import url('~nbdime/lib/styles/merge.css');
9 | @import url('~nbdime-jupyterlab/style/index.css');
10 | @import url('base.css');
11 |
--------------------------------------------------------------------------------
/style/index.js:
--------------------------------------------------------------------------------
1 | import './base.css';
2 |
--------------------------------------------------------------------------------
/style/status-widget.css:
--------------------------------------------------------------------------------
1 | .jp-git-StatusWidget {
2 | display: flex;
3 | align-items: center;
4 | }
5 |
--------------------------------------------------------------------------------
/style/variables.css:
--------------------------------------------------------------------------------
1 | /* -----------------------------------------------------------------------------
2 | | Copyright (c) Jupyter Development Team.
3 | | Distributed under the terms of the Modified BSD License.
4 | |---------------------------------------------------------------------------- */
5 |
6 | :root {
7 | --jp-git-diff-added-color: rgb(155 185 85 / 20%);
8 | --jp-git-diff-added-color1: rgb(155 185 85 / 40%);
9 | --jp-git-diff-deleted-color: rgb(255 0 0 / 20%);
10 | --jp-git-diff-deleted-color1: rgb(255 0 0 / 40%);
11 | --jp-git-diff-output-border-color: rgb(0 141 255 / 70%);
12 | --jp-git-diff-output-color: rgb(0 141 255 / 30%);
13 | --jp-merge-local-color: rgb(31 31 224 / 20%);
14 | --jp-merge-local-color1: rgb(31 31 224 / 40%);
15 | }
16 |
--------------------------------------------------------------------------------
/testutils/jest-setup-files.js:
--------------------------------------------------------------------------------
1 | /* global globalThis */
2 | globalThis.DragEvent = class DragEvent {};
3 | if (
4 | typeof globalThis.TextDecoder === 'undefined' ||
5 | typeof globalThis.TextEncoder === 'undefined'
6 | ) {
7 | const util = require('util');
8 | globalThis.TextDecoder = util.TextDecoder;
9 | globalThis.TextEncoder = util.TextEncoder;
10 | }
11 | const fetchMod = (window.fetch = require('node-fetch'));
12 | window.Request = fetchMod.Request;
13 | window.Headers = fetchMod.Headers;
14 | window.Response = fetchMod.Response;
15 | globalThis.Image = window.Image;
16 | window.focus = () => {
17 | /* JSDom throws "Not Implemented" */
18 | };
19 | window.document.elementFromPoint = (left, top) => document.body;
20 | if (!window.hasOwnProperty('getSelection')) {
21 | // Minimal getSelection() that supports a fake selection
22 | window.getSelection = function getSelection() {
23 | return {
24 | _selection: '',
25 | selectAllChildren: () => {
26 | this._selection = 'foo';
27 | },
28 | toString: () => {
29 | const val = this._selection;
30 | this._selection = '';
31 | return val;
32 | }
33 | };
34 | };
35 | }
36 | // Used by xterm.js
37 | window.matchMedia = function (media) {
38 | return {
39 | matches: false,
40 | media,
41 | onchange: () => {
42 | /* empty */
43 | },
44 | addEventListener: () => {
45 | /* empty */
46 | },
47 | removeEventListener: () => {
48 | /* empty */
49 | },
50 | dispatchEvent: () => {
51 | return true;
52 | },
53 | addListener: () => {
54 | /* empty */
55 | },
56 | removeListener: () => {
57 | /* empty */
58 | }
59 | };
60 | };
61 | process.on('unhandledRejection', (error, promise) => {
62 | console.error('Unhandled promise rejection somewhere in tests');
63 | if (error) {
64 | console.error(error);
65 | const stack = error.stack;
66 | if (stack) {
67 | console.error(stack);
68 | }
69 | }
70 | promise.catch(err => console.error('promise rejected', err));
71 | });
72 | if (window.requestIdleCallback === undefined) {
73 | // On Safari, requestIdleCallback is not available, so we use replacement functions for `idleCallbacks`
74 | // See: https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API#falling_back_to_settimeout
75 | // eslint-disable-next-line @typescript-eslint/ban-types
76 | window.requestIdleCallback = function (handler) {
77 | let startTime = Date.now();
78 | return setTimeout(function () {
79 | handler({
80 | didTimeout: false,
81 | timeRemaining: function () {
82 | return Math.max(0, 50.0 - (Date.now() - startTime));
83 | }
84 | });
85 | }, 1);
86 | };
87 | window.cancelIdleCallback = function (id) {
88 | clearTimeout(id);
89 | };
90 | }
91 |
92 | globalThis.ResizeObserver = require('resize-observer-polyfill');
93 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "composite": true,
5 | "declaration": true,
6 | "esModuleInterop": true,
7 | "incremental": true,
8 | "jsx": "react",
9 | "module": "esnext",
10 | "moduleResolution": "node",
11 | "noEmitOnError": true,
12 | "noImplicitAny": true,
13 | "noUnusedLocals": true,
14 | "preserveWatchOutput": true,
15 | "resolveJsonModule": true,
16 | "outDir": "lib",
17 | "rootDir": "src",
18 | "skipLibCheck": true,
19 | "strict": true,
20 | "strictNullChecks": true,
21 | "target": "ES2018",
22 | "types": ["resize-observer-browser"]
23 | },
24 | "include": ["src/**/*"],
25 | "exclude": ["src/__tests__"]
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "types": ["jest"]
5 | },
6 | "include": ["src/**/*"],
7 | "exclude": []
8 | }
9 |
--------------------------------------------------------------------------------
/ui-tests/jupyter_server_test_config.py:
--------------------------------------------------------------------------------
1 | """Server configuration for integration tests.
2 |
3 | !! Never use this configuration in production because it
4 | opens the server to the world and provide access to JupyterLab
5 | JavaScript objects through the global window variable.
6 | """
7 |
8 | import sys
9 |
10 | try:
11 | import jupyter_archive
12 | except ImportError:
13 | print("You must install `jupyter-archive` for the integration tests.")
14 | sys.exit(1)
15 |
16 | from jupyterlab.galata import configure_jupyter_server
17 |
18 | configure_jupyter_server(c)
19 |
20 | # Uncomment to set server log level to debug level
21 | # c.ServerApp.log_level = "DEBUG"
22 |
--------------------------------------------------------------------------------
/ui-tests/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@jupyterlab/git-ui-tests",
3 | "version": "1.0.0",
4 | "description": "JupyterLab @jupyterlab/git Integration Tests",
5 | "private": true,
6 | "scripts": {
7 | "start": "jupyter lab --config jupyter_server_test_config.py",
8 | "test": "jlpm playwright test",
9 | "test:update": "jlpm playwright test --update-snapshots"
10 | },
11 | "devDependencies": {
12 | "@jupyterlab/galata": "^5.0.6",
13 | "@playwright/test": "^1.37.0"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/ui-tests/playwright.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Configuration for Playwright using default from @jupyterlab/galata
3 | */
4 | const baseConfig = require('@jupyterlab/galata/lib/playwright-config');
5 |
6 | module.exports = {
7 | ...baseConfig,
8 | webServer: {
9 | command: 'jlpm start',
10 | url: 'http://localhost:8888/lab',
11 | timeout: 120 * 1000,
12 | reuseExistingServer: !process.env.CI
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/ui-tests/tests/add-tag.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, galata, test } from '@jupyterlab/galata';
2 | import path from 'path';
3 | import { extractFile } from './utils';
4 |
5 | const baseRepositoryPath = 'test-repository-dirty.tar.gz';
6 | test.use({ autoGoto: false, mockSettings: galata.DEFAULT_SETTINGS });
7 |
8 | test.describe('Add tag', () => {
9 | test.beforeEach(async ({ page, request, tmpPath }) => {
10 | await extractFile(
11 | request,
12 | path.resolve(__dirname, 'data', baseRepositoryPath),
13 | path.join(tmpPath, 'repository.tar.gz')
14 | );
15 |
16 | // URL for merge conflict example repository
17 | await page.goto(`tree/${tmpPath}/test-repository`);
18 |
19 | await page.sidebar.openTab('jp-git-sessions');
20 | });
21 |
22 | test('should show Add Tag command on commit from history sidebar', async ({
23 | page
24 | }) => {
25 | await page.click('button:has-text("History")');
26 |
27 | const commits = page.locator('li[title="View commit details"]');
28 |
29 | expect(await commits.count()).toBeGreaterThanOrEqual(2);
30 |
31 | // Right click the first commit to open the context menu, with the add tag command
32 | await page.getByText('master changes').click({ button: 'right' });
33 |
34 | expect(await page.getByRole('menuitem', { name: 'Add Tag' })).toBeTruthy();
35 | });
36 |
37 | test('should open new tag dialog box', async ({ page }) => {
38 | await page.click('button:has-text("History")');
39 |
40 | const commits = page.locator('li[title="View commit details"]');
41 |
42 | expect(await commits.count()).toBeGreaterThanOrEqual(2);
43 |
44 | // Right click the first commit to open the context menu, with the add tag command
45 | await page.getByText('master changes').click({ button: 'right' });
46 |
47 | // Click on the add tag command
48 | await page.getByRole('menuitem', { name: 'Add Tag' }).click();
49 |
50 | expect(page.getByText('Create a Tag')).toBeTruthy();
51 | });
52 |
53 | test('should create new tag pointing to selected commit', async ({
54 | page
55 | }) => {
56 | await page.click('button:has-text("History")');
57 |
58 | const commits = page.locator('li[title="View commit details"]');
59 | expect(await commits.count()).toBeGreaterThanOrEqual(2);
60 |
61 | // Right click the first commit to open the context menu, with the add tag command
62 | await page.getByText('master changes').click({ button: 'right' });
63 |
64 | // Click on the add tag command
65 | await page.getByRole('menuitem', { name: 'Add Tag' }).click();
66 |
67 | // Create a test tag
68 | await page.getByRole('textbox').fill('testTag');
69 | await page.getByRole('button', { name: 'Create Tag' }).click();
70 |
71 | expect(await page.getByText('testTag')).toBeTruthy();
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/ui-tests/tests/commit-diff.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@jupyterlab/galata';
2 | import path from 'path';
3 | import { extractFile } from './utils';
4 |
5 | const baseRepositoryPath = 'test-repository.tar.gz';
6 | test.use({ autoGoto: false });
7 |
8 | test.describe('Commits diff', () => {
9 | test.beforeEach(async ({ page, request, tmpPath }) => {
10 | await extractFile(
11 | request,
12 | path.resolve(__dirname, 'data', baseRepositoryPath),
13 | path.join(tmpPath, 'repository.tar.gz')
14 | );
15 |
16 | // URL for merge conflict example repository
17 | await page.goto(`tree/${tmpPath}/test-repository`);
18 | });
19 |
20 | test('should display commits diff from history', async ({ page }) => {
21 | await page.sidebar.openTab('jp-git-sessions');
22 | await page.click('button:has-text("History")');
23 | const commits = page.locator('li[title="View commit details"]');
24 |
25 | expect(await commits.count()).toBeGreaterThanOrEqual(2);
26 |
27 | await commits.last().locator('button[title="Select for compare"]').click();
28 |
29 | expect(
30 | await page.waitForSelector('text=No challenger commit selected.')
31 | ).toBeTruthy();
32 | await commits
33 | .first()
34 | .locator('button[title="Compare with selected"]')
35 | .click();
36 |
37 | expect(await page.waitForSelector('text=Changed')).toBeTruthy();
38 | });
39 |
40 | test('should display diff from single file history', async ({ page }) => {
41 | await page.sidebar.openTab('filebrowser');
42 | await page.getByText('example.ipynb').click({
43 | button: 'right'
44 | });
45 | await page.getByRole('menu').getByText('Git').hover();
46 | await page.click('#jp-contextmenu-git >> text=History');
47 |
48 | await page.waitForSelector('#jp-git-sessions >> ol >> text=example.ipynb');
49 |
50 | const commits = page.locator('li[title="View file changes"]');
51 |
52 | expect(await commits.count()).toBeGreaterThanOrEqual(2);
53 |
54 | await commits.last().locator('button[title="Select for compare"]').click();
55 | await commits
56 | .first()
57 | .locator('button[title="Compare with selected"]')
58 | .click();
59 |
60 | await expect(
61 | page.locator('.nbdime-Widget >> .jp-git-diff-banner')
62 | ).toHaveText(
63 | /79fe96219f6eaec1ae607c7c8d21d5b269a6dd29[\n\s]+51fe1f8995113884e943201341a5d5b7a1393e24/
64 | );
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/ui-tests/tests/commit.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@jupyterlab/galata';
2 | import path from 'path';
3 | import { extractFile } from './utils';
4 |
5 | const baseRepositoryPath = 'test-repository.tar.gz';
6 | test.use({ autoGoto: false });
7 |
8 | test.describe('Commit', () => {
9 | test.beforeEach(async ({ page, request, tmpPath }) => {
10 | await extractFile(
11 | request,
12 | path.resolve(__dirname, 'data', baseRepositoryPath),
13 | path.join(tmpPath, 'repository.tar.gz')
14 | );
15 |
16 | // URL for merge conflict example repository
17 | await page.goto(`tree/${tmpPath}/test-repository`);
18 | });
19 |
20 | test('should commit a change', async ({ page }) => {
21 | await page
22 | .getByRole('listitem', { name: 'Name: another_file.txt' })
23 | .dblclick();
24 | await page
25 | .getByLabel('another_file.txt')
26 | .getByRole('textbox')
27 | .fill('My new content');
28 | await page.keyboard.press('Control+s');
29 |
30 | await page.getByRole('tab', { name: 'Git' }).click();
31 | await page.getByTitle('another_file.txt • Modified').hover();
32 | await page.getByRole('button', { name: 'Stage this change' }).click();
33 |
34 | await page
35 | .getByPlaceholder('Summary (Ctrl+Enter to commit)')
36 | .fill('My new commit');
37 |
38 | await page.getByRole('button', { name: 'Commit', exact: true }).click();
39 |
40 | await page.getByRole('tab', { name: 'History' }).click();
41 |
42 | await expect(page.getByText('My new commit')).toBeVisible();
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/ui-tests/tests/data/test-repository-dirty.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlab/jupyterlab-git/c5585fdbb1c0107a719a942d0c77d913fa4ef600/ui-tests/tests/data/test-repository-dirty.tar.gz
--------------------------------------------------------------------------------
/ui-tests/tests/data/test-repository-merge-commits.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlab/jupyterlab-git/c5585fdbb1c0107a719a942d0c77d913fa4ef600/ui-tests/tests/data/test-repository-merge-commits.tar.gz
--------------------------------------------------------------------------------
/ui-tests/tests/data/test-repository-stash.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlab/jupyterlab-git/c5585fdbb1c0107a719a942d0c77d913fa4ef600/ui-tests/tests/data/test-repository-stash.tar.gz
--------------------------------------------------------------------------------
/ui-tests/tests/data/test-repository.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlab/jupyterlab-git/c5585fdbb1c0107a719a942d0c77d913fa4ef600/ui-tests/tests/data/test-repository.tar.gz
--------------------------------------------------------------------------------
/ui-tests/tests/image-diff.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@jupyterlab/galata';
2 | import path from 'path';
3 | import { extractFile } from './utils';
4 |
5 | const baseRepositoryPath = 'test-repository.tar.gz';
6 | test.use({ autoGoto: false });
7 |
8 | test.describe('Image diff', () => {
9 | test.beforeEach(async ({ page, request, tmpPath }) => {
10 | await extractFile(
11 | request,
12 | path.resolve(__dirname, 'data', baseRepositoryPath),
13 | path.join(tmpPath, 'repository.tar.gz')
14 | );
15 |
16 | // URL for merge conflict example repository
17 | await page.goto(`tree/${tmpPath}/test-repository`);
18 | });
19 |
20 | test('should display image diff from history', async ({ page }) => {
21 | await page.sidebar.openTab('jp-git-sessions');
22 | await page.click('button:has-text("History")');
23 | const commits = page.getByTitle('View commit details');
24 |
25 | await commits.first().click();
26 |
27 | await page
28 | .getByTitle('git_workflow.jpg')
29 | .getByRole('button', { name: 'View file changes' })
30 | .click();
31 |
32 | expect
33 | .soft(await page.locator('.jp-git-image-diff').screenshot())
34 | .toMatchSnapshot('jpeg_diff.png');
35 |
36 | await page
37 | .getByTitle('jupyter.png')
38 | .getByRole('button', { name: 'View file changes' })
39 | .click();
40 |
41 | expect(
42 | await page.locator('.jp-git-image-diff').last().screenshot()
43 | ).toMatchSnapshot('png_diff.png');
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/ui-tests/tests/image-diff.spec.ts-snapshots/jpeg-diff-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlab/jupyterlab-git/c5585fdbb1c0107a719a942d0c77d913fa4ef600/ui-tests/tests/image-diff.spec.ts-snapshots/jpeg-diff-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/image-diff.spec.ts-snapshots/png-diff-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlab/jupyterlab-git/c5585fdbb1c0107a719a942d0c77d913fa4ef600/ui-tests/tests/image-diff.spec.ts-snapshots/png-diff-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/merge-commit.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, galata, test } from '@jupyterlab/galata';
2 | import path from 'path';
3 | import { extractFile } from './utils';
4 |
5 | const baseRepositoryPath = 'test-repository-merge-commits.tar.gz';
6 | test.use({ autoGoto: false, mockSettings: galata.DEFAULT_SETTINGS });
7 |
8 | test.describe('Merge commit tests', () => {
9 | test.beforeEach(async ({ page, request, tmpPath }) => {
10 | await extractFile(
11 | request,
12 | path.resolve(__dirname, 'data', baseRepositoryPath),
13 | path.join(tmpPath, 'repository.tar.gz')
14 | );
15 |
16 | // URL for merge commit example repository
17 | await page.goto(`tree/${tmpPath}/test-repository-merge-commits`);
18 |
19 | await page.sidebar.openTab('jp-git-sessions');
20 |
21 | await page.getByRole('tab', { name: 'History' }).click();
22 | });
23 |
24 | test('should correctly display num files changed, insertions, and deletions', async ({
25 | page
26 | }) => {
27 | const mergeCommit = page.getByText("Merge branch 'sort-names'");
28 |
29 | await mergeCommit.click();
30 |
31 | const filesChanged = mergeCommit.getByTitle('# Files Changed');
32 | const insertions = mergeCommit.getByTitle('# Insertions');
33 | const deletions = mergeCommit.getByTitle('# Deletions');
34 |
35 | await filesChanged.waitFor();
36 |
37 | expect(await filesChanged.innerText()).toBe('3');
38 | expect(await insertions.innerText()).toBe('18240');
39 | expect(await deletions.innerText()).toBe('18239');
40 | });
41 |
42 | test('should correctly display files changed', async ({ page }) => {
43 | const mergeCommit = page.getByText("Merge branch 'sort-names'");
44 |
45 | await mergeCommit.click();
46 |
47 | const helloWorldFile = page.getByRole('listitem', {
48 | name: 'hello-world.py'
49 | });
50 | const namesFile = page.getByRole('listitem', { name: 'names.txt' });
51 | const newFile = page.getByRole('listitem', { name: 'new-file.txt' });
52 |
53 | expect(helloWorldFile).toBeTruthy();
54 | expect(namesFile).toBeTruthy();
55 | expect(newFile).toBeTruthy();
56 | });
57 |
58 | test('should diff file after clicking', async ({ page }) => {
59 | const mergeCommit = page.getByText("Merge branch 'sort-names'");
60 |
61 | await mergeCommit.click();
62 |
63 | const file = page.getByRole('listitem', { name: 'hello-world.py' });
64 | await file.click();
65 |
66 | await page
67 | .getByRole('tab', { name: 'hello-world.py' })
68 | .waitFor({ state: 'visible' });
69 |
70 | await expect(page.locator('.jp-git-diff-root')).toBeVisible();
71 | });
72 |
73 | test('should revert merge commit', async ({ page }) => {
74 | const mergeCommit = page.getByText("Merge branch 'sort-names'", {
75 | exact: true
76 | });
77 |
78 | await mergeCommit.click();
79 | await page
80 | .getByRole('button', { name: 'Revert changes introduced by this commit' })
81 | .click();
82 |
83 | const dialog = page.getByRole('dialog');
84 | await dialog.waitFor({ state: 'visible' });
85 |
86 | expect(dialog).toBeTruthy();
87 |
88 | await dialog.getByRole('button', { name: 'Submit' }).click();
89 | await dialog.waitFor({ state: 'detached' });
90 |
91 | const revertMergeCommit = page
92 | .locator('#jp-git-sessions')
93 | .getByText("Revert 'Merge branch 'sort-names''");
94 |
95 | await expect(revertMergeCommit).toBeVisible();
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/ui-tests/tests/merge-conflict.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, galata, test } from '@jupyterlab/galata';
2 | import path from 'path';
3 | import { extractFile } from './utils';
4 |
5 | const baseRepositoryPath = 'test-repository.tar.gz';
6 | test.use({ autoGoto: false, mockSettings: galata.DEFAULT_SETTINGS });
7 |
8 | test.describe('Merge conflict tests', () => {
9 | test.beforeEach(async ({ page, request, tmpPath }) => {
10 | await extractFile(
11 | request,
12 | path.resolve(__dirname, 'data', baseRepositoryPath),
13 | path.join(tmpPath, 'repository.tar.gz')
14 | );
15 |
16 | // URL for merge conflict example repository
17 | await page.goto(`tree/${tmpPath}/test-repository`);
18 |
19 | await page.sidebar.openTab('jp-git-sessions');
20 |
21 | await page.getByRole('button', { name: 'Current Branch master' }).click();
22 |
23 | // Click on a-branch merge button
24 | await page.locator('text=a-branch').hover();
25 | await page
26 | .getByRole('button', {
27 | name: 'Merge this branch into the current one',
28 | exact: true
29 | })
30 | .click();
31 |
32 | // Hide branch panel
33 | await page.getByRole('button', { name: 'Current Branch master' }).click();
34 |
35 | // Force refresh
36 | await page
37 | .getByRole('button', {
38 | name: 'Refresh the repository to detect local and remote changes'
39 | })
40 | .click();
41 | });
42 |
43 | test('should diff conflicted text file', async ({ page }) => {
44 | await page
45 | .getByTitle('file.txt • Conflicted', { exact: true })
46 | .click({ clickCount: 2 });
47 | await page.waitForSelector(
48 | '.jp-git-diff-parent-widget[id^="Current-Incoming"] .jp-spinner',
49 | { state: 'detached' }
50 | );
51 | await page.waitForSelector('.jp-git-diff-root');
52 |
53 | // Verify 3-way merge view appears
54 | const banner = page.locator('.jp-git-merge-banner');
55 | await expect(banner).toHaveText(/Current/);
56 | await expect(banner).toHaveText(/Result/);
57 | await expect(banner).toHaveText(/Incoming/);
58 |
59 | const mergeDiff = page.locator('.cm-merge-3pane');
60 | await expect(mergeDiff).toBeVisible();
61 | });
62 |
63 | test('should diff conflicted notebook file', async ({ page }) => {
64 | await page.getByTitle('example.ipynb • Conflicted').click({
65 | clickCount: 2
66 | });
67 | await page.waitForSelector(
68 | '.jp-git-diff-parent-widget[id^="Current-Incoming"] .jp-spinner',
69 | { state: 'detached' }
70 | );
71 | await page.waitForSelector('.jp-git-diff-root');
72 |
73 | // Verify notebook merge view appears
74 | const banner = page.locator('.jp-git-merge-banner');
75 | await expect(banner).toHaveText(/Current/);
76 | await expect(banner).toHaveText(/Incoming/);
77 |
78 | const mergeDiff = page.locator('.jp-Notebook-merge');
79 | await expect(mergeDiff).toBeVisible();
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/ui-tests/tests/utils.ts:
--------------------------------------------------------------------------------
1 | import { galata } from '@jupyterlab/galata';
2 | import { APIRequestContext } from '@playwright/test';
3 |
4 | export async function extractFile(
5 | request: APIRequestContext,
6 | filePath: string,
7 | destination: string
8 | ): Promise {
9 | const contents = galata.newContentsHelper(request);
10 | await contents.uploadFile(filePath, destination);
11 |
12 | await request.get(`/extract-archive/${destination}`);
13 |
14 | await contents.deleteFile(destination);
15 | }
16 |
--------------------------------------------------------------------------------