2019-10-12 21:52:04 +05:30
|
|
|
import { commitActionTypes, FILE_VIEW_MODE_EDITOR } from '../constants';
|
2020-06-23 00:09:42 +05:30
|
|
|
import {
|
|
|
|
relativePathToAbsolute,
|
|
|
|
isAbsolute,
|
|
|
|
isRootRelative,
|
|
|
|
isBase64DataUrl,
|
|
|
|
} from '~/lib/utils/url_utility';
|
2019-07-31 22:56:46 +05:30
|
|
|
|
2018-05-09 12:01:36 +05:30
|
|
|
export const dataStructure = () => ({
|
|
|
|
id: '',
|
|
|
|
// Key will contain a mixture of ID and path
|
|
|
|
// it can also contain a prefix `pending-` for files opened in review mode
|
|
|
|
key: '',
|
|
|
|
type: '',
|
|
|
|
name: '',
|
|
|
|
path: '',
|
|
|
|
tempFile: false,
|
|
|
|
tree: [],
|
|
|
|
loading: false,
|
|
|
|
opened: false,
|
|
|
|
active: false,
|
|
|
|
changed: false,
|
2018-10-15 14:42:47 +05:30
|
|
|
staged: false,
|
2018-11-08 19:23:39 +05:30
|
|
|
lastCommitSha: '',
|
2018-05-09 12:01:36 +05:30
|
|
|
rawPath: '',
|
|
|
|
raw: '',
|
|
|
|
content: '',
|
|
|
|
editorRow: 1,
|
|
|
|
editorColumn: 1,
|
|
|
|
fileLanguage: '',
|
2019-10-12 21:52:04 +05:30
|
|
|
viewMode: FILE_VIEW_MODE_EDITOR,
|
2018-05-09 12:01:36 +05:30
|
|
|
size: 0,
|
2018-10-15 14:42:47 +05:30
|
|
|
parentPath: null,
|
|
|
|
lastOpenedAt: 0,
|
|
|
|
mrChange: null,
|
2018-11-18 11:00:15 +05:30
|
|
|
deleted: false,
|
2019-12-21 20:55:43 +05:30
|
|
|
prevPath: undefined,
|
2018-05-09 12:01:36 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
export const decorateData = entity => {
|
|
|
|
const {
|
|
|
|
id,
|
|
|
|
type,
|
|
|
|
name,
|
|
|
|
path,
|
|
|
|
content = '',
|
|
|
|
tempFile = false,
|
|
|
|
active = false,
|
|
|
|
opened = false,
|
|
|
|
changed = false,
|
2019-07-31 22:56:46 +05:30
|
|
|
rawPath = '',
|
2018-05-09 12:01:36 +05:30
|
|
|
file_lock,
|
2018-10-15 14:42:47 +05:30
|
|
|
parentPath = '',
|
2018-05-09 12:01:36 +05:30
|
|
|
} = entity;
|
|
|
|
|
2019-07-07 11:18:12 +05:30
|
|
|
return Object.assign(dataStructure(), {
|
2018-05-09 12:01:36 +05:30
|
|
|
id,
|
|
|
|
key: `${name}-${type}-${id}`,
|
|
|
|
type,
|
|
|
|
name,
|
|
|
|
path,
|
|
|
|
tempFile,
|
|
|
|
opened,
|
|
|
|
active,
|
|
|
|
changed,
|
|
|
|
content,
|
2019-07-31 22:56:46 +05:30
|
|
|
rawPath,
|
2018-05-09 12:01:36 +05:30
|
|
|
file_lock,
|
2018-10-15 14:42:47 +05:30
|
|
|
parentPath,
|
2019-07-07 11:18:12 +05:30
|
|
|
});
|
2018-05-09 12:01:36 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
export const setPageTitle = title => {
|
|
|
|
document.title = title;
|
|
|
|
};
|
|
|
|
|
2019-12-26 22:10:19 +05:30
|
|
|
export const setPageTitleForFile = (state, file) => {
|
|
|
|
const title = [file.path, state.currentBranchId, state.currentProjectId, 'GitLab'].join(' · ');
|
|
|
|
setPageTitle(title);
|
|
|
|
};
|
|
|
|
|
2018-11-18 11:00:15 +05:30
|
|
|
export const commitActionForFile = file => {
|
|
|
|
if (file.prevPath) {
|
2019-07-31 22:56:46 +05:30
|
|
|
return commitActionTypes.move;
|
2018-11-18 11:00:15 +05:30
|
|
|
} else if (file.deleted) {
|
2019-07-31 22:56:46 +05:30
|
|
|
return commitActionTypes.delete;
|
2020-06-23 00:09:42 +05:30
|
|
|
} else if (file.tempFile) {
|
2019-07-31 22:56:46 +05:30
|
|
|
return commitActionTypes.create;
|
2018-11-18 11:00:15 +05:30
|
|
|
}
|
|
|
|
|
2019-07-31 22:56:46 +05:30
|
|
|
return commitActionTypes.update;
|
2018-11-18 11:00:15 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
export const getCommitFiles = stagedFiles =>
|
|
|
|
stagedFiles.reduce((acc, file) => {
|
2019-12-21 20:55:43 +05:30
|
|
|
if (file.type === 'tree') return acc;
|
2018-11-18 11:00:15 +05:30
|
|
|
|
|
|
|
return acc.concat({
|
|
|
|
...file,
|
|
|
|
});
|
|
|
|
}, []);
|
|
|
|
|
2019-07-31 22:56:46 +05:30
|
|
|
export const createCommitPayload = ({
|
|
|
|
branch,
|
|
|
|
getters,
|
|
|
|
newBranch,
|
|
|
|
state,
|
|
|
|
rootState,
|
|
|
|
rootGetters,
|
|
|
|
}) => ({
|
2018-05-09 12:01:36 +05:30
|
|
|
branch,
|
2018-11-08 19:23:39 +05:30
|
|
|
commit_message: state.commitMessage || getters.preBuiltCommitMessage,
|
2018-11-18 11:00:15 +05:30
|
|
|
actions: getCommitFiles(rootState.stagedFiles).map(f => ({
|
|
|
|
action: commitActionForFile(f),
|
2019-12-21 20:55:43 +05:30
|
|
|
file_path: f.path,
|
|
|
|
previous_path: f.prevPath || undefined,
|
|
|
|
content: f.prevPath && !f.changed ? null : f.content || undefined,
|
2020-06-23 00:09:42 +05:30
|
|
|
encoding: isBase64DataUrl(f.rawPath) ? 'base64' : 'text',
|
|
|
|
last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha,
|
2018-05-09 12:01:36 +05:30
|
|
|
})),
|
2019-10-12 21:52:04 +05:30
|
|
|
start_sha: newBranch ? rootGetters.lastCommit.id : undefined,
|
2018-05-09 12:01:36 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
export const createNewMergeRequestUrl = (projectUrl, source, target) =>
|
2020-03-13 15:44:24 +05:30
|
|
|
`${projectUrl}/-/merge_requests/new?merge_request[source_branch]=${source}&merge_request[target_branch]=${target}&nav_source=webide`;
|
2018-05-09 12:01:36 +05:30
|
|
|
|
|
|
|
const sortTreesByTypeAndName = (a, b) => {
|
|
|
|
if (a.type === 'tree' && b.type === 'blob') {
|
|
|
|
return -1;
|
|
|
|
} else if (a.type === 'blob' && b.type === 'tree') {
|
|
|
|
return 1;
|
|
|
|
}
|
2018-10-15 14:42:47 +05:30
|
|
|
if (a.name < b.name) return -1;
|
|
|
|
if (a.name > b.name) return 1;
|
2018-05-09 12:01:36 +05:30
|
|
|
return 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const sortTree = sortedTree =>
|
|
|
|
sortedTree
|
|
|
|
.map(entity =>
|
|
|
|
Object.assign(entity, {
|
|
|
|
tree: entity.tree.length ? sortTree(entity.tree) : [],
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
.sort(sortTreesByTypeAndName);
|
2018-10-15 14:42:47 +05:30
|
|
|
|
2018-11-18 11:00:15 +05:30
|
|
|
export const filePathMatches = (filePath, path) => filePath.indexOf(`${path}/`) === 0;
|
2018-10-15 14:42:47 +05:30
|
|
|
|
|
|
|
export const getChangesCountForFiles = (files, path) =>
|
2018-11-18 11:00:15 +05:30
|
|
|
files.filter(f => filePathMatches(f.path, path)).length;
|
2019-07-07 11:18:12 +05:30
|
|
|
|
|
|
|
export const mergeTrees = (fromTree, toTree) => {
|
|
|
|
if (!fromTree || !fromTree.length) {
|
|
|
|
return toTree;
|
|
|
|
}
|
|
|
|
|
|
|
|
const recurseTree = (n, t) => {
|
|
|
|
if (!n) {
|
|
|
|
return t;
|
|
|
|
}
|
|
|
|
const existingTreeNode = t.find(el => el.path === n.path);
|
|
|
|
|
|
|
|
if (existingTreeNode && n.tree.length > 0) {
|
|
|
|
existingTreeNode.opened = true;
|
|
|
|
recurseTree(n.tree[0], existingTreeNode.tree);
|
|
|
|
} else if (!existingTreeNode) {
|
|
|
|
const sorted = sortTree(t.concat(n));
|
|
|
|
t.splice(0, t.length + 1, ...sorted);
|
|
|
|
}
|
|
|
|
return t;
|
|
|
|
};
|
|
|
|
|
|
|
|
for (let i = 0, l = fromTree.length; i < l; i += 1) {
|
|
|
|
recurseTree(fromTree[i], toTree);
|
|
|
|
}
|
|
|
|
|
|
|
|
return toTree;
|
|
|
|
};
|
2019-12-21 20:55:43 +05:30
|
|
|
|
|
|
|
export const swapInStateArray = (state, arr, key, entryPath) =>
|
|
|
|
Object.assign(state, {
|
|
|
|
[arr]: state[arr].map(f => (f.key === key ? state.entries[entryPath] : f)),
|
|
|
|
});
|
|
|
|
|
|
|
|
export const getEntryOrRoot = (state, path) =>
|
|
|
|
path ? state.entries[path] : state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
|
|
|
|
|
|
|
|
export const swapInParentTreeWithSorting = (state, oldKey, newPath, parentPath) => {
|
|
|
|
if (!newPath) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const parent = getEntryOrRoot(state, parentPath);
|
|
|
|
|
|
|
|
if (parent) {
|
|
|
|
const tree = parent.tree
|
|
|
|
// filter out old entry && new entry
|
|
|
|
.filter(({ key, path }) => key !== oldKey && path !== newPath)
|
|
|
|
// concat new entry
|
|
|
|
.concat(state.entries[newPath]);
|
|
|
|
|
|
|
|
parent.tree = sortTree(tree);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
export const removeFromParentTree = (state, oldKey, parentPath) => {
|
|
|
|
const parent = getEntryOrRoot(state, parentPath);
|
|
|
|
|
|
|
|
if (parent) {
|
|
|
|
parent.tree = sortTree(parent.tree.filter(({ key }) => key !== oldKey));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
export const updateFileCollections = (state, key, entryPath) => {
|
|
|
|
['openFiles', 'changedFiles', 'stagedFiles'].forEach(fileCollection => {
|
|
|
|
swapInStateArray(state, fileCollection, key, entryPath);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
export const cleanTrailingSlash = path => path.replace(/\/$/, '');
|
|
|
|
|
|
|
|
export const pathsAreEqual = (a, b) => {
|
|
|
|
const cleanA = a ? cleanTrailingSlash(a) : '';
|
|
|
|
const cleanB = b ? cleanTrailingSlash(b) : '';
|
|
|
|
|
|
|
|
return cleanA === cleanB;
|
|
|
|
};
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
export function extractMarkdownImagesFromEntries(mdFile, entries) {
|
|
|
|
/**
|
|
|
|
* Regex to identify an image tag in markdown, like:
|
|
|
|
*
|
|
|
|
* ![img alt goes here](/img.png)
|
|
|
|
* ![img alt](../img 1/img.png "my image title")
|
|
|
|
* ![img alt](https://gitlab.com/assets/logo.svg "title here")
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
const reMdImage = /!\[([^\]]*)\]\((.*?)(?:(?="|\))"([^"]*)")?\)/gi;
|
|
|
|
const prefix = 'gl_md_img_';
|
|
|
|
const images = {};
|
|
|
|
|
|
|
|
let content = mdFile.content || mdFile.raw;
|
|
|
|
let i = 0;
|
|
|
|
|
|
|
|
content = content.replace(reMdImage, (_, alt, path, title) => {
|
|
|
|
const imagePath = (isRootRelative(path) ? path : relativePathToAbsolute(path, mdFile.path))
|
|
|
|
.substr(1)
|
|
|
|
.trim();
|
|
|
|
|
|
|
|
const imageContent = entries[imagePath]?.content || entries[imagePath]?.raw;
|
|
|
|
|
|
|
|
if (!isAbsolute(path) && imageContent) {
|
|
|
|
const ext = path.includes('.')
|
|
|
|
? path
|
|
|
|
.split('.')
|
|
|
|
.pop()
|
|
|
|
.trim()
|
|
|
|
: 'jpeg';
|
|
|
|
const src = `data:image/${ext};base64,${imageContent}`;
|
|
|
|
i += 1;
|
|
|
|
const key = `{{${prefix}${i}}}`;
|
|
|
|
images[key] = { alt, src, title };
|
|
|
|
return key;
|
|
|
|
}
|
|
|
|
return title ? `![${alt}](${path}"${title}")` : `![${alt}](${path})`;
|
|
|
|
});
|
|
|
|
|
|
|
|
return { content, images };
|
|
|
|
}
|