mirror of
https://github.com/antos-rde/antosdk-apps.git
synced 2024-12-25 11:48:21 +01:00
Add gitgrap app & library
This commit is contained in:
parent
c994d1c738
commit
5f0588ee27
File diff suppressed because one or more lines are too long
645
GitGrapth/LibGitGraph.ts
Normal file
645
GitGrapth/LibGitGraph.ts
Normal file
@ -0,0 +1,645 @@
|
||||
namespace OS {
|
||||
export namespace API
|
||||
{
|
||||
export interface LibGitGraphOptions {
|
||||
commits_per_page?: number,
|
||||
x_offset?: number,
|
||||
y_offset?: number,
|
||||
target: HTMLElement,
|
||||
popup_height?: number
|
||||
}
|
||||
interface CommitData
|
||||
{
|
||||
hashes: {
|
||||
commit: string,
|
||||
parents: string,
|
||||
tree: string
|
||||
},
|
||||
author: {
|
||||
date: string,
|
||||
name: string,
|
||||
email: string
|
||||
},
|
||||
committer:{
|
||||
date: string,
|
||||
name: string,
|
||||
email: string
|
||||
},
|
||||
extra: string,
|
||||
message: string,
|
||||
branches?: string[],
|
||||
// helper values for redering
|
||||
domel?: HTMLParagraphElement,
|
||||
cx?: number,
|
||||
color?: string
|
||||
};
|
||||
interface LineData
|
||||
{
|
||||
x_offset: number,
|
||||
y_offset: number,
|
||||
next_commit: string,
|
||||
current_commit: string,
|
||||
beginning: boolean,
|
||||
color: string,
|
||||
}
|
||||
type OpenDiffCallback = (file: VFS.BaseFileHandle[]) => void;
|
||||
interface LineConverging
|
||||
{
|
||||
src: LineData[],
|
||||
dest: LineData
|
||||
}
|
||||
export class LibGitGraph
|
||||
{
|
||||
private _base_dir: VFS.BaseFileHandle;
|
||||
private options: LibGitGraphOptions;
|
||||
private lines_data: LineData[];
|
||||
private commits: GenericObject<CommitData>;
|
||||
private oldest_commit_date: string;
|
||||
private svg_element: SVGElement;
|
||||
private commits_list_element: HTMLDivElement;
|
||||
private current_y_offset: number;
|
||||
private load_more_el: HTMLParagraphElement;
|
||||
private commit_detail_el: HTMLDivElement;
|
||||
private current_head: CommitData;
|
||||
private _on_open_diff:OpenDiffCallback;
|
||||
constructor(option: LibGitGraphOptions)
|
||||
{
|
||||
this._base_dir = undefined;
|
||||
this.lines_data = [];
|
||||
this.commits = {};
|
||||
this.oldest_commit_date = undefined;
|
||||
this.svg_element = undefined;
|
||||
this.commits_list_element = undefined;
|
||||
this.load_more_el = undefined;
|
||||
this.commit_detail_el = undefined;
|
||||
this.current_head = undefined;
|
||||
this._on_open_diff = undefined;
|
||||
this.options = {
|
||||
commits_per_page: 100,
|
||||
x_offset: 24,
|
||||
y_offset: 24,
|
||||
target: undefined,
|
||||
popup_height: 250
|
||||
};
|
||||
for(const k in option)
|
||||
{
|
||||
this.options[k] = option[k];
|
||||
}
|
||||
this.current_y_offset = this.options.y_offset;
|
||||
this.init_graph();
|
||||
}
|
||||
set base_dir(v: VFS.BaseFileHandle)
|
||||
{
|
||||
this._base_dir = v;
|
||||
this.render_next();
|
||||
|
||||
}
|
||||
private gen_color(x:number): string
|
||||
{
|
||||
let n = x + 11;
|
||||
const rgb = [0, 0, 0];
|
||||
for (let i = 0; i < 24; i++) {
|
||||
rgb[i%3] <<= 1;
|
||||
rgb[i%3] |= n & 0x01;
|
||||
n >>= 1;
|
||||
}
|
||||
return '#' + rgb.reduce((a, c) => (c > 0x0f ? c.toString(16) : '0' + c.toString(16)) + a, '');
|
||||
|
||||
}
|
||||
private meta(): PackageMetaType
|
||||
{
|
||||
return OS.setting.system.packages['GitGraph'];
|
||||
}
|
||||
private call(request: GenericObject<any>): Promise<any> {
|
||||
return new Promise(async (ok, reject) => {
|
||||
request.args.base_dir = this._base_dir.path;
|
||||
let cmd = {
|
||||
path: this.meta().path + "/api/api.lua",
|
||||
parameters: request
|
||||
}
|
||||
let data = await API.apigateway(cmd, false);
|
||||
if(!data.error)
|
||||
{
|
||||
ok(data.result);
|
||||
}
|
||||
else
|
||||
{
|
||||
reject(API.throwe(__("LibGitGrapth server call error: {0}", data.error)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private load(before?: string): Promise<any>
|
||||
{
|
||||
let request = {
|
||||
action: 'log',
|
||||
args: {
|
||||
n_commits: this.options.commits_per_page.toString(),
|
||||
before: before?before:null
|
||||
}
|
||||
}
|
||||
return this.call(request);
|
||||
}
|
||||
private error(e: Error)
|
||||
{
|
||||
announcer.oserror(__("GitGraph error: {0}", e.toString()), e);
|
||||
}
|
||||
set on_open_diff(c: OpenDiffCallback)
|
||||
{
|
||||
this._on_open_diff = c;
|
||||
}
|
||||
private init_graph()
|
||||
{
|
||||
if(!this.options.target)
|
||||
{
|
||||
return this.error(API.throwe("Target element is undefined"));
|
||||
}
|
||||
$(this.options.target)
|
||||
.css("overflow-y", "auto")
|
||||
.css("overflow-x", "hidden")
|
||||
.css("display", "block")
|
||||
.css("position", "relative");
|
||||
this.svg_element = this.make_svg_el("svg",{
|
||||
width: this.options.x_offset,
|
||||
height: this.options.y_offset
|
||||
});
|
||||
$(this.svg_element)
|
||||
.css("display", "block")
|
||||
.css("position", "absolute")
|
||||
.css("left", "0")
|
||||
//s.css("z-index", 10)
|
||||
.css("top", "0");
|
||||
$(this.options.target).empty();
|
||||
this.options.target.appendChild(this.svg_element);
|
||||
const div = $("<div />")
|
||||
.css("position", "absolute")
|
||||
.css("left", "0")
|
||||
.css("top", "0")
|
||||
.css("width", "100%")
|
||||
.css("padding-top",`${this.options.y_offset / 2}px`);
|
||||
this.commits_list_element = div[0] as HTMLDivElement;
|
||||
this.options.target.appendChild(this.commits_list_element);
|
||||
|
||||
const p = $("<p />")
|
||||
.css("height", `${this.options.y_offset}px`)
|
||||
.css("display", "block")
|
||||
.css("padding", "0")
|
||||
.css("margin", "0")
|
||||
.css("line-height",`${this.options.y_offset}px`)
|
||||
.css("vertical-align", "middle");
|
||||
p.addClass("git_grapth_load_more");
|
||||
p.on("click", (e) => this.render_next());
|
||||
p.text(__("More").__());
|
||||
this.load_more_el = p[0] as HTMLParagraphElement;
|
||||
this.commits_list_element.appendChild(this.load_more_el);
|
||||
|
||||
const popup = $("<div />")
|
||||
.css("position", "absolute")
|
||||
.css("top", "0")
|
||||
.css("height", this.options.popup_height + "px")
|
||||
.css("display", "none")
|
||||
.css("user-select", "text")
|
||||
.addClass("git_grapth_commit_detail");
|
||||
this.commit_detail_el = popup[0] as HTMLDivElement;
|
||||
this.options.target.appendChild(this.commit_detail_el);
|
||||
|
||||
}
|
||||
private render_next()
|
||||
{
|
||||
this.load(this.oldest_commit_date)
|
||||
.then((data: CommitData[]) =>
|
||||
{
|
||||
if(this.oldest_commit_date)
|
||||
{
|
||||
// remove the first commit as it is already in
|
||||
// the graph
|
||||
data.shift();
|
||||
}
|
||||
this.draw_graph(data);
|
||||
})
|
||||
.catch(e=>this.error(e))
|
||||
}
|
||||
|
||||
private make_svg_el(tag: string, attrs: GenericObject<any>) {
|
||||
const el= document.createElementNS('http://www.w3.org/2000/svg', tag);
|
||||
for (var k in attrs)
|
||||
el.setAttribute(k, attrs[k]);
|
||||
return el;
|
||||
}
|
||||
private max_line_off_x():number
|
||||
{
|
||||
if(this.lines_data.length == 0)
|
||||
return 0;
|
||||
return Math.max.apply(Math, this.lines_data.map((o) => o.x_offset))
|
||||
}
|
||||
private update_line_data(commit: CommitData, y_offset: number): LineConverging
|
||||
{
|
||||
const parent_commits = commit.hashes.parents.split(" ");
|
||||
// get the list of child lines
|
||||
const children = this.lines_data.filter((line) => line.next_commit == commit.hashes.commit);
|
||||
let merge: LineConverging = {
|
||||
src: [],
|
||||
dest: undefined
|
||||
}
|
||||
if (children.length === 0 )
|
||||
{
|
||||
// add new line
|
||||
let line: LineData = {
|
||||
next_commit: parent_commits[0],
|
||||
x_offset: this.max_line_off_x() + this.options.x_offset,
|
||||
current_commit: commit.hashes.commit,
|
||||
beginning: true,
|
||||
y_offset: y_offset ,
|
||||
color: this.gen_color(this.lines_data.length),
|
||||
};
|
||||
this.lines_data.push(line);
|
||||
merge.dest = line;
|
||||
}
|
||||
else
|
||||
{
|
||||
let min_offset_x = Math.min.apply(Math, children.map((o) => o.x_offset));
|
||||
let line: LineData = undefined;
|
||||
for (let el of children){
|
||||
if(el.x_offset == min_offset_x)
|
||||
{
|
||||
line = el;
|
||||
line.next_commit = parent_commits[0];
|
||||
line.current_commit = commit.hashes.commit;
|
||||
line.y_offset = y_offset;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.lines_data.splice(this.lines_data.indexOf(el), 1);
|
||||
merge.src.push(el);
|
||||
}
|
||||
}
|
||||
merge.dest = line;
|
||||
}
|
||||
if(parent_commits.length === 2)
|
||||
{
|
||||
let line: LineData = undefined;
|
||||
line = this.lines_data.filter(l=>l.next_commit == parent_commits[1])[0];
|
||||
if(!line)
|
||||
{
|
||||
// add new line
|
||||
line = {
|
||||
next_commit: parent_commits[1],
|
||||
x_offset: this.max_line_off_x() + this.options.x_offset,
|
||||
current_commit: commit.hashes.commit,
|
||||
beginning: true,
|
||||
y_offset: y_offset + this.options.y_offset,
|
||||
color: this.gen_color(this.lines_data.length),
|
||||
};
|
||||
this.lines_data.push(line);
|
||||
}
|
||||
else
|
||||
{
|
||||
line.y_offset = y_offset + this.options.y_offset;
|
||||
}
|
||||
merge.src.push(line);
|
||||
}
|
||||
return merge;
|
||||
}
|
||||
private draw_line(_x1: number,_y1: number, _x2: number, _y2: number, color: string, stroke?:number): SVGElement
|
||||
{
|
||||
let line_opt: GenericObject<any> ={
|
||||
stroke: color,
|
||||
fill: 'none',
|
||||
"stroke-width": 1.5
|
||||
};
|
||||
if(stroke)
|
||||
{
|
||||
line_opt['stroke-width'] = stroke;
|
||||
}
|
||||
if(_x1 == _x2)
|
||||
{
|
||||
line_opt.d = `M ${_x1},${_y1} L ${_x2},${_y2}`;
|
||||
}
|
||||
else
|
||||
{
|
||||
let x1 = _x1;
|
||||
let y1 = _y1;
|
||||
let x2 = _x2;
|
||||
let y2 = _y2;
|
||||
let dx = Math.abs(x2-x1);
|
||||
let dy = Math.abs(y2 -y1);
|
||||
if(_y1 < _y2)
|
||||
{
|
||||
x1 = _x2;
|
||||
y1 = _y2;
|
||||
x2 = _x1;
|
||||
y2 = _y1;
|
||||
}
|
||||
line_opt.d = `M ${x1},${y1} C ${x1},${y1 - dy} ${x2},${y2 + dy} ${x2},${y2}`;
|
||||
}
|
||||
const line = this.make_svg_el("path", line_opt);
|
||||
return line;
|
||||
}
|
||||
private gen_commit_data_header(name: string| FormattedString, value: string): HTMLParagraphElement
|
||||
{
|
||||
const p = $("<p />")
|
||||
.css("display","block");
|
||||
p[0].innerHTML = `<b>${name.__()}</b>: ${value}`;
|
||||
return p[0] as HTMLParagraphElement;
|
||||
}
|
||||
private open_popup(commit: CommitData)
|
||||
{
|
||||
const el = commit.domel;
|
||||
if(!el) return;
|
||||
$(this.commit_detail_el).empty();
|
||||
const position = $(el).position();
|
||||
const bbox = (this.svg_element as SVGAElement).getBBox();
|
||||
const off_left = bbox.x + bbox.width + this.options.x_offset / 2;
|
||||
const svg = this.make_svg_el("svg",{
|
||||
width: off_left-commit.cx + 5,
|
||||
height: this.options.y_offset
|
||||
});
|
||||
$(svg)
|
||||
.css("display", "block")
|
||||
.css("position", "absolute")
|
||||
.css("left", "0")
|
||||
.css("top", "-2px");
|
||||
svg.appendChild(this.draw_line(
|
||||
0, this.options.y_offset/2,
|
||||
off_left-commit.cx, this.options.y_offset/2,
|
||||
commit.color,
|
||||
));
|
||||
/*
|
||||
svg.appendChild(
|
||||
this.make_svg_el("circle",{
|
||||
cx: off_left-commit.cx - 1,
|
||||
cy: this.options.y_offset/2,
|
||||
r:4,
|
||||
fill: commit.color,
|
||||
"stroke-width": 0.0
|
||||
})
|
||||
);*/
|
||||
$(this.commit_detail_el)
|
||||
.css("border", "2px solid " + commit.color)
|
||||
.css("color", commit.color)
|
||||
.append(
|
||||
$("<div />")
|
||||
.css("position","absolute")
|
||||
.css("height", this.options.y_offset)
|
||||
.css("left", commit.cx-off_left)
|
||||
.css("padding-left", off_left-commit.cx)
|
||||
.append(svg)
|
||||
.append($("<i/>").text('[X]')
|
||||
.css("cursor", "pointer"))
|
||||
.addClass("git_grapth_commit_detail_ctrl")
|
||||
.on("click",(e) =>{
|
||||
$(this.commit_detail_el)
|
||||
.css("display", "none")
|
||||
.empty();
|
||||
}));
|
||||
const left = $("<div />")
|
||||
.css("display", "block")
|
||||
.css("overflow-y", "auto")
|
||||
.css("overflow-x", "hidden")
|
||||
.css("flex", "1")
|
||||
.css("border-right", "1px solid " + commit.color)
|
||||
.addClass("git_grapth_commit_detail_left");
|
||||
const right = $("<div />")
|
||||
.css("display", "block")
|
||||
.css("overflow-y", "auto")
|
||||
.css("overflow-x", "hidden")
|
||||
.css("flex", "1")
|
||||
.addClass("git_grapth_commit_detail_right");
|
||||
// display
|
||||
left.append(this.gen_commit_data_header(__("Commit"), commit.hashes.commit));
|
||||
left.append(this.gen_commit_data_header(__("Parents"), commit.hashes.parents));
|
||||
left.append(this.gen_commit_data_header(__("Author"), `${commit.committer.name} <${commit.committer.email}>`));
|
||||
left.append(this.gen_commit_data_header(__("Date"), (new Date(commit.committer.date).toDateString()) ));
|
||||
left.append(
|
||||
$("<pre />")
|
||||
.css("white-space", "pre-wrap")
|
||||
.text(commit.message));
|
||||
this.commit_detail_el.appendChild(left[0]);
|
||||
this.commit_detail_el.appendChild(right[0]);
|
||||
this.list_file(commit.hashes.commit)
|
||||
.then((files) =>
|
||||
{
|
||||
const ul = $('<ul/>');
|
||||
$.each(files, (index, value) => {
|
||||
const arr = value.split("\t");
|
||||
const li = $('<li/>');
|
||||
const a = $('<a/>')
|
||||
.css("cursor", "pointer")
|
||||
.addClass(`git_graph_file_${arr[0].toLowerCase()}`)
|
||||
.on("click",e => {
|
||||
if(this._on_open_diff)
|
||||
{
|
||||
Promise.all([
|
||||
this.get_file(arr[1], `${commit.hashes.commit}^`),
|
||||
this.get_file(arr[1], commit.hashes.commit)
|
||||
])
|
||||
.then((values) => {
|
||||
// create the file
|
||||
const files = values.map((content, index) =>{
|
||||
const file = `mem://${commit.hashes.commit.slice(0,8)}${index==0?"^":""}/${arr[1]}`.asFileHandle();
|
||||
file.cache = content;
|
||||
file.info.mime = "text/plain";
|
||||
return file;
|
||||
});
|
||||
this._on_open_diff(files);
|
||||
})
|
||||
.catch((e) => {
|
||||
announcer.oserror(
|
||||
__(
|
||||
"Unable to fetch diff of {0}: {1}",
|
||||
commit.hashes.commit,
|
||||
e.toString()
|
||||
),e );
|
||||
})
|
||||
}
|
||||
});
|
||||
a.text(arr[1]);
|
||||
ul.append(li.append(a));
|
||||
});
|
||||
right.append(ul);
|
||||
})
|
||||
.catch((e) => announcer.oserror(__("Unable to get commit changes: {0}", e.toString()),e))
|
||||
|
||||
// scroll down if necessary
|
||||
$(this.commit_detail_el)
|
||||
.css("top", position.top)
|
||||
.css("left", off_left)
|
||||
.css("display", "flex")
|
||||
.css("width", `calc(100% - ${off_left + this.options.x_offset}px)`)
|
||||
.css("fflex-direction", "row");
|
||||
const delta = this.commit_detail_el.getBoundingClientRect().bottom -
|
||||
this.options.target.getBoundingClientRect().bottom;
|
||||
if(delta > 0)
|
||||
{
|
||||
this.options.target.scrollTop += delta + 10;
|
||||
}
|
||||
}
|
||||
private render_commit(commit: CommitData, color: string): boolean
|
||||
{
|
||||
let current = false;
|
||||
const p = $("<p />")
|
||||
.css("padding","0")
|
||||
.css("margin","0")
|
||||
.css("display","block")
|
||||
.css("height",`${this.options.y_offset}px`)
|
||||
.css("line-height",`${this.options.y_offset}px`)
|
||||
.css("color", color)
|
||||
.css("vertical-align", "middle")
|
||||
.css("white-space", "nowrap")
|
||||
.css("overflow", "hidden");
|
||||
p.addClass("git_graph_commit_message");
|
||||
let html = `<i class="git_graph_commit_hash">${commit.hashes.commit.slice(0,8)}</i> `;
|
||||
commit.branches = [];
|
||||
for (const tag of commit.extra.split(",").map(e=>e.trim()).filter(e=>e != ""))
|
||||
{
|
||||
let found = tag.match(/HEAD -> (.*)/i);
|
||||
if(found && found.length == 2)
|
||||
{
|
||||
html += `<i class = "git_graph_commit_branch">${found[1]}</i> `;
|
||||
p.addClass("git_graph_commit_current_head");
|
||||
current = true
|
||||
commit.branches.push(found[1]);
|
||||
this.current_head = commit;
|
||||
}
|
||||
else if((found = tag.match(/tag: (.*)/i)))
|
||||
{
|
||||
html += `<i class = "git_graph_commit_tag">${found[1]}</i> `;
|
||||
}
|
||||
else
|
||||
{
|
||||
html += `<i class = "git_graph_commit_branch">${tag}</i> `;
|
||||
commit.branches.push(tag);
|
||||
}
|
||||
}
|
||||
html += `<i class ="git_graph_commit_text">${commit.message.split("\n")[0]}</i> `;
|
||||
html += `<i class ="git_graph_commit_author">${commit.committer.name}</i> `;
|
||||
html += `<i class ="git_graph_commit_date">${new Date(commit.committer.date).toDateString()}</i>`;
|
||||
p[0].innerHTML = html;
|
||||
p.on("click", (e) => {
|
||||
this.open_popup(commit);
|
||||
});
|
||||
this.commits_list_element.insertBefore(p[0], this.load_more_el);
|
||||
commit.domel = p[0] as HTMLParagraphElement;
|
||||
return current;
|
||||
}
|
||||
private draw_graph(data:CommitData[])
|
||||
{
|
||||
for(const commit of data)
|
||||
{
|
||||
this.oldest_commit_date = commit.committer.date;
|
||||
if(commit.extra.includes("refs/stash"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
this.commits[commit.hashes.commit] = commit;
|
||||
let merge = this.update_line_data(commit, this.current_y_offset);
|
||||
let lines = merge.src;
|
||||
// combine the lines
|
||||
for(const linedata of lines.filter(o=> o.x_offset != merge.dest.x_offset))
|
||||
{
|
||||
this.svg_element.appendChild(
|
||||
this.draw_line(
|
||||
merge.dest.x_offset,
|
||||
merge.dest.y_offset,
|
||||
linedata.x_offset,linedata.y_offset,
|
||||
linedata.color)
|
||||
);
|
||||
}
|
||||
this.lines_data.sort((a,b) => a.x_offset - b.x_offset);
|
||||
let x_offset = this.options.x_offset;
|
||||
for(const linedata of this.lines_data)
|
||||
{
|
||||
if(linedata.beginning && linedata.y_offset > this.current_y_offset)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
linedata.y_offset = this.current_y_offset;
|
||||
if(!linedata.beginning)
|
||||
{
|
||||
if(linedata.x_offset > x_offset)
|
||||
{
|
||||
this.svg_element.appendChild(
|
||||
this.draw_line(linedata.x_offset ,
|
||||
linedata.y_offset - this.options.y_offset,
|
||||
linedata.x_offset - this.options.x_offset,
|
||||
linedata.y_offset,
|
||||
linedata.color));
|
||||
linedata.x_offset = x_offset;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.svg_element.appendChild(
|
||||
this.draw_line(linedata.x_offset,
|
||||
linedata.y_offset - this.options.y_offset,
|
||||
linedata.x_offset,
|
||||
linedata.y_offset,
|
||||
linedata.color));
|
||||
|
||||
}
|
||||
}
|
||||
x_offset += this.options.x_offset;
|
||||
linedata.beginning = false;
|
||||
}
|
||||
let options = {
|
||||
cx: merge.dest.x_offset,
|
||||
cy: merge.dest.y_offset,
|
||||
r:4,
|
||||
fill: merge.dest.color,
|
||||
stroke: 'black',
|
||||
"stroke-width": 0.0
|
||||
};
|
||||
if(this.render_commit(commit, merge.dest.color))
|
||||
{
|
||||
options.r = options.r * 1.5;
|
||||
}
|
||||
commit.cx = options.cx;
|
||||
commit.color = options.fill;
|
||||
const circle = this.make_svg_el("circle",options);
|
||||
this.svg_element.appendChild(circle);
|
||||
this.current_y_offset += this.options.y_offset;
|
||||
|
||||
}
|
||||
const bbox = (this.svg_element as SVGAElement).getBBox();
|
||||
this.svg_element.setAttribute("width", (bbox.x + bbox.width ).toString());
|
||||
this.svg_element.setAttribute("height", (bbox.y + bbox.height + this.options.y_offset/ 2).toString());
|
||||
//$(this.commits_list_element).css("left", `-${bbox.x + bbox.width}px`);
|
||||
$(".git_graph_commit_message", this.commits_list_element).css("padding-left", `${bbox.x + bbox.width +this.options.x_offset / 2}px`)
|
||||
}
|
||||
|
||||
list_file(hash: string): Promise<string[]>
|
||||
{
|
||||
let request = {
|
||||
action: 'list_file',
|
||||
args: {
|
||||
commit: hash
|
||||
}
|
||||
}
|
||||
return this.call(request);
|
||||
}
|
||||
|
||||
get_changes(file: string, hash: string): Promise<string>
|
||||
{
|
||||
let request = {
|
||||
action: 'get_changes',
|
||||
args: {
|
||||
commit: hash,
|
||||
file: file
|
||||
}
|
||||
}
|
||||
return this.call(request);
|
||||
}
|
||||
|
||||
get_file(file: string, hash: string): Promise<string>
|
||||
{
|
||||
let request = {
|
||||
action: 'get_file',
|
||||
args: {
|
||||
commit: hash,
|
||||
file: file
|
||||
}
|
||||
}
|
||||
return this.call(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
GitGrapth/README.md
Normal file
15
GitGrapth/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# LibGitGraph
|
||||
This is an example project, generated by AntOS Development Kit
|
||||
|
||||
## Howto
|
||||
Use the Antedit command palette to access to the SDK functionalities:
|
||||
|
||||
1. Create new project
|
||||
2. Init the project from the current folder located in side bar
|
||||
3. Build and run the project
|
||||
4. Release the project in zip package
|
||||
|
||||
## Set up build target
|
||||
|
||||
Open the `build.json` file from the current project tree and add/remove
|
||||
build target entries and jobs. Save the file
|
86
GitGrapth/api/api.lua
Normal file
86
GitGrapth/api/api.lua
Normal file
@ -0,0 +1,86 @@
|
||||
-- nothing todo for now
|
||||
local args=...
|
||||
local vfs = require("vfs")
|
||||
|
||||
local result = function(data)
|
||||
return { error = false, result = data }
|
||||
end
|
||||
|
||||
local error = function(msg)
|
||||
return {error = msg, result = false}
|
||||
end
|
||||
|
||||
local handle = {}
|
||||
|
||||
handle.log = function(data)
|
||||
-- convert VFS to OS path
|
||||
local os_path = vfs.ospath(data.base_dir)
|
||||
if(not os_path) then
|
||||
return error("Base dir "..data.base_dir.." is not found")
|
||||
end
|
||||
local cmd = "cd "..os_path.." && "
|
||||
cmd = cmd.."git --no-pager log -n "..data.n_commits.." --all --format='{ \"hashes\":{ \"commit\":\"%H\", \"tree\":\"%T\", \"parents\":\"%P\" }, \"author\":{ \"date\": \"%ai\", \"name\": \"%an\", \"email\":\"%ae\" }, \"committer\":{ \"date\": \"%ci\", \"name\": \"%cn\", \"email\":\"%ce\" }, \"extra\":\"%D\"} ====:RAW:====%B====:RAW:========:COMMITEND:===='"
|
||||
if(data.before) then
|
||||
cmd = cmd.." --before='"..data.before.."'"
|
||||
end
|
||||
local f = assert(io.popen(cmd, 'r'))
|
||||
local s = assert(f:read('*a'))
|
||||
f:close()
|
||||
local ret = {}
|
||||
for line in s:gmatch("(.-)====:COMMITEND:====\n") do
|
||||
local arr = {}
|
||||
for el in line:gmatch("(.-)====:RAW:====") do
|
||||
table.insert(arr, el)
|
||||
end
|
||||
local commit = JSON.decodeString(arr[1])
|
||||
commit.message = arr[2]
|
||||
table.insert(ret, commit)
|
||||
end
|
||||
return result(ret)
|
||||
end
|
||||
|
||||
handle.list_file = function(data)
|
||||
local os_path = vfs.ospath(data.base_dir)
|
||||
if(not os_path) then
|
||||
return error("Base dir "..data.base_dir.." is not found")
|
||||
end
|
||||
local cmd = "cd "..os_path..' && git --no-pager log -m -1 --name-status --pretty="format:" '..data.commit
|
||||
local f = assert(io.popen(cmd, 'r'))
|
||||
local s = assert(f:read('*a'))
|
||||
f:close()
|
||||
local ret = {}
|
||||
for line in s:gmatch("(.-)\n") do
|
||||
table.insert(ret, line)
|
||||
end
|
||||
return result(ret)
|
||||
end
|
||||
|
||||
handle.get_changes = function(data)
|
||||
local os_path = vfs.ospath(data.base_dir)
|
||||
if(not os_path) then
|
||||
return error("Base dir "..data.base_dir.." is not found")
|
||||
end
|
||||
local cmd = "cd "..os_path.." && git --no-pager diff --no-prefix -U1000 "..data.commit.."^:"..data.file.." "..data.commit..":"..data.file
|
||||
local f = assert(io.popen(cmd, 'r'))
|
||||
local s = assert(f:read('*a'))
|
||||
f:close()
|
||||
return result(s)
|
||||
end
|
||||
|
||||
handle.get_file = function(data)
|
||||
local os_path = vfs.ospath(data.base_dir)
|
||||
if(not os_path) then
|
||||
return error("Base dir "..data.base_dir.." is not found")
|
||||
end
|
||||
local cmd = "cd "..os_path.." && git --no-pager show "..data.commit..":"..data.file
|
||||
local f = assert(io.popen(cmd, 'r'))
|
||||
local s = assert(f:read('*a'))
|
||||
f:close()
|
||||
return result(s)
|
||||
end
|
||||
|
||||
if args.action and handle[args.action] then
|
||||
return handle[args.action](args.args)
|
||||
else
|
||||
return error("Invalid action parameter")
|
||||
end
|
99
GitGrapth/build.json
Normal file
99
GitGrapth/build.json
Normal file
@ -0,0 +1,99 @@
|
||||
{
|
||||
"name": "GitGraph",
|
||||
"targets":{
|
||||
"clean": {
|
||||
"jobs": [
|
||||
{
|
||||
"name": "vfs-rm",
|
||||
"data": ["build/debug","build/release"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"require": ["ts"],
|
||||
"jobs":[
|
||||
{
|
||||
"name": "vfs-mkdir",
|
||||
"data": ["build","build/debug","build/release"]
|
||||
},
|
||||
{
|
||||
"name": "ts-import",
|
||||
"data": ["sdk://core/ts/core.d.ts", "sdk://core/ts/jquery.d.ts","sdk://core/ts/antos.d.ts"]
|
||||
},
|
||||
{
|
||||
"name": "ts-compile",
|
||||
"data": {
|
||||
"src": ["main.ts"],
|
||||
"dest": "build/debug/main.js"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ts-compile",
|
||||
"data": {
|
||||
"src": ["LibGitGraph.ts"],
|
||||
"dest": "build/debug/libgitgraph.js"
|
||||
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"uglify": {
|
||||
"require": ["terser"],
|
||||
"jobs": [
|
||||
{
|
||||
"name":"terser-uglify",
|
||||
"data": ["build/debug/main.js"]
|
||||
},
|
||||
{
|
||||
"name":"terser-uglify",
|
||||
"data": ["build/debug/libgitgraph.js"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"copy": {
|
||||
"jobs": [
|
||||
{
|
||||
"name": "vfs-cp",
|
||||
"data": {
|
||||
"src": [
|
||||
"scheme.html",
|
||||
"package.json",
|
||||
"README.md",
|
||||
"api",
|
||||
"main.css"
|
||||
],
|
||||
"dest":"build/debug"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"debug" : {
|
||||
"depend": ["clean","build", "copy"],
|
||||
"jobs": [
|
||||
{
|
||||
"name": "vfs-cat",
|
||||
"data": {
|
||||
"src": [
|
||||
"build/debug/main.js",
|
||||
"build/debug/libgitgraph.js"
|
||||
],
|
||||
"dest":"build/debug/main.js"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"depend": ["clean","build","uglify", "copy"],
|
||||
"require": ["zip"],
|
||||
"jobs": [
|
||||
{
|
||||
"name": "zip-mk",
|
||||
"data": {
|
||||
"src":"build/debug",
|
||||
"dest":"build/release/GitGraph.zip"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
15
GitGrapth/build/debug/README.md
Normal file
15
GitGrapth/build/debug/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# LibGitGraph
|
||||
This is an example project, generated by AntOS Development Kit
|
||||
|
||||
## Howto
|
||||
Use the Antedit command palette to access to the SDK functionalities:
|
||||
|
||||
1. Create new project
|
||||
2. Init the project from the current folder located in side bar
|
||||
3. Build and run the project
|
||||
4. Release the project in zip package
|
||||
|
||||
## Set up build target
|
||||
|
||||
Open the `build.json` file from the current project tree and add/remove
|
||||
build target entries and jobs. Save the file
|
86
GitGrapth/build/debug/api/api.lua
Normal file
86
GitGrapth/build/debug/api/api.lua
Normal file
@ -0,0 +1,86 @@
|
||||
-- nothing todo for now
|
||||
local args=...
|
||||
local vfs = require("vfs")
|
||||
|
||||
local result = function(data)
|
||||
return { error = false, result = data }
|
||||
end
|
||||
|
||||
local error = function(msg)
|
||||
return {error = msg, result = false}
|
||||
end
|
||||
|
||||
local handle = {}
|
||||
|
||||
handle.log = function(data)
|
||||
-- convert VFS to OS path
|
||||
local os_path = vfs.ospath(data.base_dir)
|
||||
if(not os_path) then
|
||||
return error("Base dir "..data.base_dir.." is not found")
|
||||
end
|
||||
local cmd = "cd "..os_path.." && "
|
||||
cmd = cmd.."git --no-pager log -n "..data.n_commits.." --all --format='{ \"hashes\":{ \"commit\":\"%H\", \"tree\":\"%T\", \"parents\":\"%P\" }, \"author\":{ \"date\": \"%ai\", \"name\": \"%an\", \"email\":\"%ae\" }, \"committer\":{ \"date\": \"%ci\", \"name\": \"%cn\", \"email\":\"%ce\" }, \"extra\":\"%D\"} ====:RAW:====%B====:RAW:========:COMMITEND:===='"
|
||||
if(data.before) then
|
||||
cmd = cmd.." --before='"..data.before.."'"
|
||||
end
|
||||
local f = assert(io.popen(cmd, 'r'))
|
||||
local s = assert(f:read('*a'))
|
||||
f:close()
|
||||
local ret = {}
|
||||
for line in s:gmatch("(.-)====:COMMITEND:====\n") do
|
||||
local arr = {}
|
||||
for el in line:gmatch("(.-)====:RAW:====") do
|
||||
table.insert(arr, el)
|
||||
end
|
||||
local commit = JSON.decodeString(arr[1])
|
||||
commit.message = arr[2]
|
||||
table.insert(ret, commit)
|
||||
end
|
||||
return result(ret)
|
||||
end
|
||||
|
||||
handle.list_file = function(data)
|
||||
local os_path = vfs.ospath(data.base_dir)
|
||||
if(not os_path) then
|
||||
return error("Base dir "..data.base_dir.." is not found")
|
||||
end
|
||||
local cmd = "cd "..os_path..' && git --no-pager log -m -1 --name-status --pretty="format:" '..data.commit
|
||||
local f = assert(io.popen(cmd, 'r'))
|
||||
local s = assert(f:read('*a'))
|
||||
f:close()
|
||||
local ret = {}
|
||||
for line in s:gmatch("(.-)\n") do
|
||||
table.insert(ret, line)
|
||||
end
|
||||
return result(ret)
|
||||
end
|
||||
|
||||
handle.get_changes = function(data)
|
||||
local os_path = vfs.ospath(data.base_dir)
|
||||
if(not os_path) then
|
||||
return error("Base dir "..data.base_dir.." is not found")
|
||||
end
|
||||
local cmd = "cd "..os_path.." && git --no-pager diff --no-prefix -U1000 "..data.commit.."^:"..data.file.." "..data.commit..":"..data.file
|
||||
local f = assert(io.popen(cmd, 'r'))
|
||||
local s = assert(f:read('*a'))
|
||||
f:close()
|
||||
return result(s)
|
||||
end
|
||||
|
||||
handle.get_file = function(data)
|
||||
local os_path = vfs.ospath(data.base_dir)
|
||||
if(not os_path) then
|
||||
return error("Base dir "..data.base_dir.." is not found")
|
||||
end
|
||||
local cmd = "cd "..os_path.." && git --no-pager show "..data.commit..":"..data.file
|
||||
local f = assert(io.popen(cmd, 'r'))
|
||||
local s = assert(f:read('*a'))
|
||||
f:close()
|
||||
return result(s)
|
||||
end
|
||||
|
||||
if args.action and handle[args.action] then
|
||||
return handle[args.action](args.args)
|
||||
else
|
||||
return error("Invalid action parameter")
|
||||
end
|
498
GitGrapth/build/debug/libgitgraph.js
Normal file
498
GitGrapth/build/debug/libgitgraph.js
Normal file
@ -0,0 +1,498 @@
|
||||
|
||||
var OS;
|
||||
(function (OS) {
|
||||
let API;
|
||||
(function (API) {
|
||||
;
|
||||
class LibGitGraph {
|
||||
constructor(option) {
|
||||
this._base_dir = undefined;
|
||||
this.lines_data = [];
|
||||
this.commits = {};
|
||||
this.oldest_commit_date = undefined;
|
||||
this.svg_element = undefined;
|
||||
this.commits_list_element = undefined;
|
||||
this.load_more_el = undefined;
|
||||
this.commit_detail_el = undefined;
|
||||
this.current_head = undefined;
|
||||
this._on_open_diff = undefined;
|
||||
this.options = {
|
||||
commits_per_page: 100,
|
||||
x_offset: 24,
|
||||
y_offset: 24,
|
||||
target: undefined,
|
||||
popup_height: 250
|
||||
};
|
||||
for (const k in option) {
|
||||
this.options[k] = option[k];
|
||||
}
|
||||
this.current_y_offset = this.options.y_offset;
|
||||
this.init_graph();
|
||||
}
|
||||
set base_dir(v) {
|
||||
this._base_dir = v;
|
||||
this.render_next();
|
||||
}
|
||||
gen_color(x) {
|
||||
let n = x + 11;
|
||||
const rgb = [0, 0, 0];
|
||||
for (let i = 0; i < 24; i++) {
|
||||
rgb[i % 3] <<= 1;
|
||||
rgb[i % 3] |= n & 0x01;
|
||||
n >>= 1;
|
||||
}
|
||||
return '#' + rgb.reduce((a, c) => (c > 0x0f ? c.toString(16) : '0' + c.toString(16)) + a, '');
|
||||
}
|
||||
meta() {
|
||||
return OS.setting.system.packages['GitGraph'];
|
||||
}
|
||||
call(request) {
|
||||
return new Promise(async (ok, reject) => {
|
||||
request.args.base_dir = this._base_dir.path;
|
||||
let cmd = {
|
||||
path: this.meta().path + "/api/api.lua",
|
||||
parameters: request
|
||||
};
|
||||
let data = await API.apigateway(cmd, false);
|
||||
if (!data.error) {
|
||||
ok(data.result);
|
||||
}
|
||||
else {
|
||||
reject(API.throwe(__("LibGitGrapth server call error: {0}", data.error)));
|
||||
}
|
||||
});
|
||||
}
|
||||
load(before) {
|
||||
let request = {
|
||||
action: 'log',
|
||||
args: {
|
||||
n_commits: this.options.commits_per_page.toString(),
|
||||
before: before ? before : null
|
||||
}
|
||||
};
|
||||
return this.call(request);
|
||||
}
|
||||
error(e) {
|
||||
OS.announcer.oserror(__("GitGraph error: {0}", e.toString()), e);
|
||||
}
|
||||
set on_open_diff(c) {
|
||||
this._on_open_diff = c;
|
||||
}
|
||||
init_graph() {
|
||||
if (!this.options.target) {
|
||||
return this.error(API.throwe("Target element is undefined"));
|
||||
}
|
||||
$(this.options.target)
|
||||
.css("overflow-y", "auto")
|
||||
.css("overflow-x", "hidden")
|
||||
.css("display", "block")
|
||||
.css("position", "relative");
|
||||
this.svg_element = this.make_svg_el("svg", {
|
||||
width: this.options.x_offset,
|
||||
height: this.options.y_offset
|
||||
});
|
||||
$(this.svg_element)
|
||||
.css("display", "block")
|
||||
.css("position", "absolute")
|
||||
.css("left", "0")
|
||||
//s.css("z-index", 10)
|
||||
.css("top", "0");
|
||||
$(this.options.target).empty();
|
||||
this.options.target.appendChild(this.svg_element);
|
||||
const div = $("<div />")
|
||||
.css("position", "absolute")
|
||||
.css("left", "0")
|
||||
.css("top", "0")
|
||||
.css("width", "100%")
|
||||
.css("padding-top", `${this.options.y_offset / 2}px`);
|
||||
this.commits_list_element = div[0];
|
||||
this.options.target.appendChild(this.commits_list_element);
|
||||
const p = $("<p />")
|
||||
.css("height", `${this.options.y_offset}px`)
|
||||
.css("display", "block")
|
||||
.css("padding", "0")
|
||||
.css("margin", "0")
|
||||
.css("line-height", `${this.options.y_offset}px`)
|
||||
.css("vertical-align", "middle");
|
||||
p.addClass("git_grapth_load_more");
|
||||
p.on("click", (e) => this.render_next());
|
||||
p.text(__("More").__());
|
||||
this.load_more_el = p[0];
|
||||
this.commits_list_element.appendChild(this.load_more_el);
|
||||
const popup = $("<div />")
|
||||
.css("position", "absolute")
|
||||
.css("top", "0")
|
||||
.css("height", this.options.popup_height + "px")
|
||||
.css("display", "none")
|
||||
.css("user-select", "text")
|
||||
.addClass("git_grapth_commit_detail");
|
||||
this.commit_detail_el = popup[0];
|
||||
this.options.target.appendChild(this.commit_detail_el);
|
||||
}
|
||||
render_next() {
|
||||
this.load(this.oldest_commit_date)
|
||||
.then((data) => {
|
||||
if (this.oldest_commit_date) {
|
||||
// remove the first commit as it is already in
|
||||
// the graph
|
||||
data.shift();
|
||||
}
|
||||
this.draw_graph(data);
|
||||
})
|
||||
.catch(e => this.error(e));
|
||||
}
|
||||
make_svg_el(tag, attrs) {
|
||||
const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
|
||||
for (var k in attrs)
|
||||
el.setAttribute(k, attrs[k]);
|
||||
return el;
|
||||
}
|
||||
max_line_off_x() {
|
||||
if (this.lines_data.length == 0)
|
||||
return 0;
|
||||
return Math.max.apply(Math, this.lines_data.map((o) => o.x_offset));
|
||||
}
|
||||
update_line_data(commit, y_offset) {
|
||||
const parent_commits = commit.hashes.parents.split(" ");
|
||||
// get the list of child lines
|
||||
const children = this.lines_data.filter((line) => line.next_commit == commit.hashes.commit);
|
||||
let merge = {
|
||||
src: [],
|
||||
dest: undefined
|
||||
};
|
||||
if (children.length === 0) {
|
||||
// add new line
|
||||
let line = {
|
||||
next_commit: parent_commits[0],
|
||||
x_offset: this.max_line_off_x() + this.options.x_offset,
|
||||
current_commit: commit.hashes.commit,
|
||||
beginning: true,
|
||||
y_offset: y_offset,
|
||||
color: this.gen_color(this.lines_data.length),
|
||||
};
|
||||
this.lines_data.push(line);
|
||||
merge.dest = line;
|
||||
}
|
||||
else {
|
||||
let min_offset_x = Math.min.apply(Math, children.map((o) => o.x_offset));
|
||||
let line = undefined;
|
||||
for (let el of children) {
|
||||
if (el.x_offset == min_offset_x) {
|
||||
line = el;
|
||||
line.next_commit = parent_commits[0];
|
||||
line.current_commit = commit.hashes.commit;
|
||||
line.y_offset = y_offset;
|
||||
}
|
||||
else {
|
||||
this.lines_data.splice(this.lines_data.indexOf(el), 1);
|
||||
merge.src.push(el);
|
||||
}
|
||||
}
|
||||
merge.dest = line;
|
||||
}
|
||||
if (parent_commits.length === 2) {
|
||||
let line = undefined;
|
||||
line = this.lines_data.filter(l => l.next_commit == parent_commits[1])[0];
|
||||
if (!line) {
|
||||
// add new line
|
||||
line = {
|
||||
next_commit: parent_commits[1],
|
||||
x_offset: this.max_line_off_x() + this.options.x_offset,
|
||||
current_commit: commit.hashes.commit,
|
||||
beginning: true,
|
||||
y_offset: y_offset + this.options.y_offset,
|
||||
color: this.gen_color(this.lines_data.length),
|
||||
};
|
||||
this.lines_data.push(line);
|
||||
}
|
||||
else {
|
||||
line.y_offset = y_offset + this.options.y_offset;
|
||||
}
|
||||
merge.src.push(line);
|
||||
}
|
||||
return merge;
|
||||
}
|
||||
draw_line(_x1, _y1, _x2, _y2, color, stroke) {
|
||||
let line_opt = {
|
||||
stroke: color,
|
||||
fill: 'none',
|
||||
"stroke-width": 1.5
|
||||
};
|
||||
if (stroke) {
|
||||
line_opt['stroke-width'] = stroke;
|
||||
}
|
||||
if (_x1 == _x2) {
|
||||
line_opt.d = `M ${_x1},${_y1} L ${_x2},${_y2}`;
|
||||
}
|
||||
else {
|
||||
let x1 = _x1;
|
||||
let y1 = _y1;
|
||||
let x2 = _x2;
|
||||
let y2 = _y2;
|
||||
let dx = Math.abs(x2 - x1);
|
||||
let dy = Math.abs(y2 - y1);
|
||||
if (_y1 < _y2) {
|
||||
x1 = _x2;
|
||||
y1 = _y2;
|
||||
x2 = _x1;
|
||||
y2 = _y1;
|
||||
}
|
||||
line_opt.d = `M ${x1},${y1} C ${x1},${y1 - dy} ${x2},${y2 + dy} ${x2},${y2}`;
|
||||
}
|
||||
const line = this.make_svg_el("path", line_opt);
|
||||
return line;
|
||||
}
|
||||
gen_commit_data_header(name, value) {
|
||||
const p = $("<p />")
|
||||
.css("display", "block");
|
||||
p[0].innerHTML = `<b>${name.__()}</b>: ${value}`;
|
||||
return p[0];
|
||||
}
|
||||
open_popup(commit) {
|
||||
const el = commit.domel;
|
||||
if (!el)
|
||||
return;
|
||||
$(this.commit_detail_el).empty();
|
||||
const position = $(el).position();
|
||||
const bbox = this.svg_element.getBBox();
|
||||
const off_left = bbox.x + bbox.width + this.options.x_offset / 2;
|
||||
const svg = this.make_svg_el("svg", {
|
||||
width: off_left - commit.cx + 5,
|
||||
height: this.options.y_offset
|
||||
});
|
||||
$(svg)
|
||||
.css("display", "block")
|
||||
.css("position", "absolute")
|
||||
.css("left", "0")
|
||||
.css("top", "-2px");
|
||||
svg.appendChild(this.draw_line(0, this.options.y_offset / 2, off_left - commit.cx, this.options.y_offset / 2, commit.color));
|
||||
/*
|
||||
svg.appendChild(
|
||||
this.make_svg_el("circle",{
|
||||
cx: off_left-commit.cx - 1,
|
||||
cy: this.options.y_offset/2,
|
||||
r:4,
|
||||
fill: commit.color,
|
||||
"stroke-width": 0.0
|
||||
})
|
||||
);*/
|
||||
$(this.commit_detail_el)
|
||||
.css("border", "2px solid " + commit.color)
|
||||
.css("color", commit.color)
|
||||
.append($("<div />")
|
||||
.css("position", "absolute")
|
||||
.css("height", this.options.y_offset)
|
||||
.css("left", commit.cx - off_left)
|
||||
.css("padding-left", off_left - commit.cx)
|
||||
.append(svg)
|
||||
.append($("<i/>").text('[X]')
|
||||
.css("cursor", "pointer"))
|
||||
.addClass("git_grapth_commit_detail_ctrl")
|
||||
.on("click", (e) => {
|
||||
$(this.commit_detail_el)
|
||||
.css("display", "none")
|
||||
.empty();
|
||||
}));
|
||||
const left = $("<div />")
|
||||
.css("display", "block")
|
||||
.css("overflow-y", "auto")
|
||||
.css("overflow-x", "hidden")
|
||||
.css("flex", "1")
|
||||
.css("border-right", "1px solid " + commit.color)
|
||||
.addClass("git_grapth_commit_detail_left");
|
||||
const right = $("<div />")
|
||||
.css("display", "block")
|
||||
.css("overflow-y", "auto")
|
||||
.css("overflow-x", "hidden")
|
||||
.css("flex", "1")
|
||||
.addClass("git_grapth_commit_detail_right");
|
||||
// display
|
||||
left.append(this.gen_commit_data_header(__("Commit"), commit.hashes.commit));
|
||||
left.append(this.gen_commit_data_header(__("Parents"), commit.hashes.parents));
|
||||
left.append(this.gen_commit_data_header(__("Author"), `${commit.committer.name} <${commit.committer.email}>`));
|
||||
left.append(this.gen_commit_data_header(__("Date"), (new Date(commit.committer.date).toDateString())));
|
||||
left.append($("<pre />")
|
||||
.css("white-space", "pre-wrap")
|
||||
.text(commit.message));
|
||||
this.commit_detail_el.appendChild(left[0]);
|
||||
this.commit_detail_el.appendChild(right[0]);
|
||||
this.list_file(commit.hashes.commit)
|
||||
.then((files) => {
|
||||
const ul = $('<ul/>');
|
||||
$.each(files, (index, value) => {
|
||||
const arr = value.split("\t");
|
||||
const li = $('<li/>');
|
||||
const a = $('<a/>')
|
||||
.css("cursor", "pointer")
|
||||
.addClass(`git_graph_file_${arr[0].toLowerCase()}`)
|
||||
.on("click", e => {
|
||||
if (this._on_open_diff) {
|
||||
Promise.all([
|
||||
this.get_file(arr[1], `${commit.hashes.commit}^`),
|
||||
this.get_file(arr[1], commit.hashes.commit)
|
||||
])
|
||||
.then((values) => {
|
||||
// create the file
|
||||
const files = values.map((content, index) => {
|
||||
const file = `mem://${commit.hashes.commit.slice(0, 8)}${index == 0 ? "^" : ""}/${arr[1]}`.asFileHandle();
|
||||
file.cache = content;
|
||||
file.info.mime = "text/plain";
|
||||
return file;
|
||||
});
|
||||
this._on_open_diff(files);
|
||||
})
|
||||
.catch((e) => {
|
||||
OS.announcer.oserror(__("Unable to fetch diff of {0}: {1}", commit.hashes.commit, e.toString()), e);
|
||||
});
|
||||
}
|
||||
});
|
||||
a.text(arr[1]);
|
||||
ul.append(li.append(a));
|
||||
});
|
||||
right.append(ul);
|
||||
})
|
||||
.catch((e) => OS.announcer.oserror(__("Unable to get commit changes: {0}", e.toString()), e));
|
||||
// scroll down if necessary
|
||||
$(this.commit_detail_el)
|
||||
.css("top", position.top)
|
||||
.css("left", off_left)
|
||||
.css("display", "flex")
|
||||
.css("width", `calc(100% - ${off_left + this.options.x_offset}px)`)
|
||||
.css("fflex-direction", "row");
|
||||
const delta = this.commit_detail_el.getBoundingClientRect().bottom -
|
||||
this.options.target.getBoundingClientRect().bottom;
|
||||
if (delta > 0) {
|
||||
this.options.target.scrollTop += delta + 10;
|
||||
}
|
||||
}
|
||||
render_commit(commit, color) {
|
||||
let current = false;
|
||||
const p = $("<p />")
|
||||
.css("padding", "0")
|
||||
.css("margin", "0")
|
||||
.css("display", "block")
|
||||
.css("height", `${this.options.y_offset}px`)
|
||||
.css("line-height", `${this.options.y_offset}px`)
|
||||
.css("color", color)
|
||||
.css("vertical-align", "middle")
|
||||
.css("white-space", "nowrap")
|
||||
.css("overflow", "hidden");
|
||||
p.addClass("git_graph_commit_message");
|
||||
let html = `<i class="git_graph_commit_hash">${commit.hashes.commit.slice(0, 8)}</i> `;
|
||||
commit.branches = [];
|
||||
for (const tag of commit.extra.split(",").map(e => e.trim()).filter(e => e != "")) {
|
||||
let found = tag.match(/HEAD -> (.*)/i);
|
||||
if (found && found.length == 2) {
|
||||
html += `<i class = "git_graph_commit_branch">${found[1]}</i> `;
|
||||
p.addClass("git_graph_commit_current_head");
|
||||
current = true;
|
||||
commit.branches.push(found[1]);
|
||||
this.current_head = commit;
|
||||
}
|
||||
else if ((found = tag.match(/tag: (.*)/i))) {
|
||||
html += `<i class = "git_graph_commit_tag">${found[1]}</i> `;
|
||||
}
|
||||
else {
|
||||
html += `<i class = "git_graph_commit_branch">${tag}</i> `;
|
||||
commit.branches.push(tag);
|
||||
}
|
||||
}
|
||||
html += `<i class ="git_graph_commit_text">${commit.message.split("\n")[0]}</i> `;
|
||||
html += `<i class ="git_graph_commit_author">${commit.committer.name}</i> `;
|
||||
html += `<i class ="git_graph_commit_date">${new Date(commit.committer.date).toDateString()}</i>`;
|
||||
p[0].innerHTML = html;
|
||||
p.on("click", (e) => {
|
||||
this.open_popup(commit);
|
||||
});
|
||||
this.commits_list_element.insertBefore(p[0], this.load_more_el);
|
||||
commit.domel = p[0];
|
||||
return current;
|
||||
}
|
||||
draw_graph(data) {
|
||||
for (const commit of data) {
|
||||
this.oldest_commit_date = commit.committer.date;
|
||||
if (commit.extra.includes("refs/stash")) {
|
||||
continue;
|
||||
}
|
||||
this.commits[commit.hashes.commit] = commit;
|
||||
let merge = this.update_line_data(commit, this.current_y_offset);
|
||||
let lines = merge.src;
|
||||
// combine the lines
|
||||
for (const linedata of lines.filter(o => o.x_offset != merge.dest.x_offset)) {
|
||||
this.svg_element.appendChild(this.draw_line(merge.dest.x_offset, merge.dest.y_offset, linedata.x_offset, linedata.y_offset, linedata.color));
|
||||
}
|
||||
this.lines_data.sort((a, b) => a.x_offset - b.x_offset);
|
||||
let x_offset = this.options.x_offset;
|
||||
for (const linedata of this.lines_data) {
|
||||
if (linedata.beginning && linedata.y_offset > this.current_y_offset) {
|
||||
continue;
|
||||
}
|
||||
linedata.y_offset = this.current_y_offset;
|
||||
if (!linedata.beginning) {
|
||||
if (linedata.x_offset > x_offset) {
|
||||
this.svg_element.appendChild(this.draw_line(linedata.x_offset, linedata.y_offset - this.options.y_offset, linedata.x_offset - this.options.x_offset, linedata.y_offset, linedata.color));
|
||||
linedata.x_offset = x_offset;
|
||||
}
|
||||
else {
|
||||
this.svg_element.appendChild(this.draw_line(linedata.x_offset, linedata.y_offset - this.options.y_offset, linedata.x_offset, linedata.y_offset, linedata.color));
|
||||
}
|
||||
}
|
||||
x_offset += this.options.x_offset;
|
||||
linedata.beginning = false;
|
||||
}
|
||||
let options = {
|
||||
cx: merge.dest.x_offset,
|
||||
cy: merge.dest.y_offset,
|
||||
r: 4,
|
||||
fill: merge.dest.color,
|
||||
stroke: 'black',
|
||||
"stroke-width": 0.0
|
||||
};
|
||||
if (this.render_commit(commit, merge.dest.color)) {
|
||||
options.r = options.r * 1.5;
|
||||
}
|
||||
commit.cx = options.cx;
|
||||
commit.color = options.fill;
|
||||
const circle = this.make_svg_el("circle", options);
|
||||
this.svg_element.appendChild(circle);
|
||||
this.current_y_offset += this.options.y_offset;
|
||||
}
|
||||
const bbox = this.svg_element.getBBox();
|
||||
this.svg_element.setAttribute("width", (bbox.x + bbox.width).toString());
|
||||
this.svg_element.setAttribute("height", (bbox.y + bbox.height + this.options.y_offset / 2).toString());
|
||||
//$(this.commits_list_element).css("left", `-${bbox.x + bbox.width}px`);
|
||||
$(".git_graph_commit_message", this.commits_list_element).css("padding-left", `${bbox.x + bbox.width + this.options.x_offset / 2}px`);
|
||||
}
|
||||
list_file(hash) {
|
||||
let request = {
|
||||
action: 'list_file',
|
||||
args: {
|
||||
commit: hash
|
||||
}
|
||||
};
|
||||
return this.call(request);
|
||||
}
|
||||
get_changes(file, hash) {
|
||||
let request = {
|
||||
action: 'get_changes',
|
||||
args: {
|
||||
commit: hash,
|
||||
file: file
|
||||
}
|
||||
};
|
||||
return this.call(request);
|
||||
}
|
||||
get_file(file, hash) {
|
||||
let request = {
|
||||
action: 'get_file',
|
||||
args: {
|
||||
commit: hash,
|
||||
file: file
|
||||
}
|
||||
};
|
||||
return this.call(request);
|
||||
}
|
||||
}
|
||||
API.LibGitGraph = LibGitGraph;
|
||||
})(API = OS.API || (OS.API = {}));
|
||||
})(OS || (OS = {}));
|
201
GitGrapth/build/debug/main.css
Normal file
201
GitGrapth/build/debug/main.css
Normal file
@ -0,0 +1,201 @@
|
||||
afx-app-window[data-id="GitGraph"] div[data-id="git-graph"]
|
||||
{
|
||||
overflow-y: auto;
|
||||
background-color: bisque;
|
||||
}
|
||||
|
||||
p.git_graph_commit_message:hover
|
||||
{
|
||||
background-color:rgba(39, 39, 39,0.5);
|
||||
}
|
||||
p.git_graph_commit_current_head
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p.git_graph_commit_message i.git_graph_commit_hash
|
||||
{
|
||||
font-weight: bold;
|
||||
margin-right: 5px;
|
||||
font-style: normal;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
p.git_graph_commit_message i.git_graph_commit_tag,
|
||||
p.git_graph_commit_message i.git_graph_commit_branch
|
||||
{
|
||||
border: 1px solid;
|
||||
font-style: normal;
|
||||
border-radius: 5px;
|
||||
line-height: 20px;
|
||||
padding: 0;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
p.git_graph_commit_message i.git_graph_commit_tag::before
|
||||
{
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F5AF";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
|
||||
p.git_graph_commit_message i.git_graph_commit_branch::before
|
||||
{
|
||||
font-family: 'FontAwesome';
|
||||
content: "\f126";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
|
||||
div.git_grapth_commit_detail
|
||||
{
|
||||
border-radius: 5px;
|
||||
background-color: whitesmoke;
|
||||
box-shadow: 0px 3px 6px 0px rgb(0,0,0,0.5);
|
||||
}
|
||||
|
||||
div.git_grapth_commit_detail_right,
|
||||
div.git_grapth_commit_detail_left {
|
||||
background-color: transparent !important;
|
||||
padding: 10px;
|
||||
}
|
||||
div.git_grapth_commit_detail_left {
|
||||
padding-top: 20px;
|
||||
}
|
||||
div.git_grapth_commit_detail_left p{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
word-wrap:break-word;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
div.git_grapth_commit_detail_left pre
|
||||
{
|
||||
margin: 0;
|
||||
padding-top: 10px;
|
||||
display: block;
|
||||
border-top: 1px solid #9c9c9c;
|
||||
}
|
||||
div.git_grapth_commit_detail_right a{
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
|
||||
div.git_grapth_commit_detail_right a.git_graph_file_m::before
|
||||
{
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F364";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
color: blue !important;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
div.git_grapth_commit_detail_right a.git_graph_file_a::before
|
||||
{
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F37D";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
color: green !important;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
div.git_grapth_commit_detail_right a.git_graph_file_d::before
|
||||
{
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F368";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
color: red !important;
|
||||
padding-right: 5px;
|
||||
}
|
||||
div.git_grapth_commit_detail_ctrl i::before{
|
||||
display: block;
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F622";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
div.git_grapth_commit_detail_ctrl i {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
div.git_grapth_commit_detail_right ul{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
p.git_graph_commit_message i.git_graph_commit_text
|
||||
{
|
||||
font-style: normal;
|
||||
display: inline-block;
|
||||
}
|
||||
p.git_graph_commit_message i.git_graph_commit_author::before
|
||||
{
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F4DC";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
padding-left: 5px;
|
||||
padding-right: 3px;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
line-height: 20px;
|
||||
}
|
||||
p.git_graph_commit_message i.git_graph_commit_date::before
|
||||
{
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F205";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
padding-right: 3px;
|
||||
padding-left: 5px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
line-height: 20px;
|
||||
}
|
||||
p.git_grapth_load_more
|
||||
{
|
||||
font-weight: normal ;
|
||||
text-align: left;
|
||||
color:#131313;
|
||||
display: inline-block !important;
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
|
||||
p.git_grapth_load_more::before
|
||||
{
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F118";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
p.git_grapth_load_more:hover
|
||||
{
|
||||
cursor: pointer;
|
||||
}
|
538
GitGrapth/build/debug/main.js
Normal file
538
GitGrapth/build/debug/main.js
Normal file
@ -0,0 +1,538 @@
|
||||
|
||||
|
||||
var OS;
|
||||
(function (OS) {
|
||||
let API;
|
||||
(function (API) {
|
||||
})(API = OS.API || (OS.API = {}));
|
||||
let application;
|
||||
(function (application) {
|
||||
/**
|
||||
*
|
||||
* @class GitGraph
|
||||
* @extends {BaseApplication}
|
||||
*/
|
||||
class GitGraph extends application.BaseApplication {
|
||||
constructor(args) {
|
||||
super("GitGraph", args);
|
||||
}
|
||||
main() {
|
||||
let graph = new API.LibGitGraph({
|
||||
target: this.find("git-graph")
|
||||
});
|
||||
graph.on_open_diff = (files) => {
|
||||
console.log(files);
|
||||
OS.PM.processes.Antedit[0].openDiff(files);
|
||||
/*
|
||||
this._gui.launch("Antedit", [])
|
||||
.then((p) =>{
|
||||
p.observable.one("rendered",() =>(p as any).openDiff(files));
|
||||
});
|
||||
*/
|
||||
};
|
||||
graph.base_dir = "home://workspace/antos/".asFileHandle();
|
||||
}
|
||||
}
|
||||
application.GitGraph = GitGraph;
|
||||
//GitGraph.dependencies = ["pkg://GitGraph/libgitgraph.js"];
|
||||
})(application = OS.application || (OS.application = {}));
|
||||
})(OS || (OS = {}));
|
||||
|
||||
|
||||
var OS;
|
||||
(function (OS) {
|
||||
let API;
|
||||
(function (API) {
|
||||
;
|
||||
class LibGitGraph {
|
||||
constructor(option) {
|
||||
this._base_dir = undefined;
|
||||
this.lines_data = [];
|
||||
this.commits = {};
|
||||
this.oldest_commit_date = undefined;
|
||||
this.svg_element = undefined;
|
||||
this.commits_list_element = undefined;
|
||||
this.load_more_el = undefined;
|
||||
this.commit_detail_el = undefined;
|
||||
this.current_head = undefined;
|
||||
this._on_open_diff = undefined;
|
||||
this.options = {
|
||||
commits_per_page: 100,
|
||||
x_offset: 24,
|
||||
y_offset: 24,
|
||||
target: undefined,
|
||||
popup_height: 250
|
||||
};
|
||||
for (const k in option) {
|
||||
this.options[k] = option[k];
|
||||
}
|
||||
this.current_y_offset = this.options.y_offset;
|
||||
this.init_graph();
|
||||
}
|
||||
set base_dir(v) {
|
||||
this._base_dir = v;
|
||||
this.render_next();
|
||||
}
|
||||
gen_color(x) {
|
||||
let n = x + 11;
|
||||
const rgb = [0, 0, 0];
|
||||
for (let i = 0; i < 24; i++) {
|
||||
rgb[i % 3] <<= 1;
|
||||
rgb[i % 3] |= n & 0x01;
|
||||
n >>= 1;
|
||||
}
|
||||
return '#' + rgb.reduce((a, c) => (c > 0x0f ? c.toString(16) : '0' + c.toString(16)) + a, '');
|
||||
}
|
||||
meta() {
|
||||
return OS.setting.system.packages['GitGraph'];
|
||||
}
|
||||
call(request) {
|
||||
return new Promise(async (ok, reject) => {
|
||||
request.args.base_dir = this._base_dir.path;
|
||||
let cmd = {
|
||||
path: this.meta().path + "/api/api.lua",
|
||||
parameters: request
|
||||
};
|
||||
let data = await API.apigateway(cmd, false);
|
||||
if (!data.error) {
|
||||
ok(data.result);
|
||||
}
|
||||
else {
|
||||
reject(API.throwe(__("LibGitGrapth server call error: {0}", data.error)));
|
||||
}
|
||||
});
|
||||
}
|
||||
load(before) {
|
||||
let request = {
|
||||
action: 'log',
|
||||
args: {
|
||||
n_commits: this.options.commits_per_page.toString(),
|
||||
before: before ? before : null
|
||||
}
|
||||
};
|
||||
return this.call(request);
|
||||
}
|
||||
error(e) {
|
||||
OS.announcer.oserror(__("GitGraph error: {0}", e.toString()), e);
|
||||
}
|
||||
set on_open_diff(c) {
|
||||
this._on_open_diff = c;
|
||||
}
|
||||
init_graph() {
|
||||
if (!this.options.target) {
|
||||
return this.error(API.throwe("Target element is undefined"));
|
||||
}
|
||||
$(this.options.target)
|
||||
.css("overflow-y", "auto")
|
||||
.css("overflow-x", "hidden")
|
||||
.css("display", "block")
|
||||
.css("position", "relative");
|
||||
this.svg_element = this.make_svg_el("svg", {
|
||||
width: this.options.x_offset,
|
||||
height: this.options.y_offset
|
||||
});
|
||||
$(this.svg_element)
|
||||
.css("display", "block")
|
||||
.css("position", "absolute")
|
||||
.css("left", "0")
|
||||
//s.css("z-index", 10)
|
||||
.css("top", "0");
|
||||
$(this.options.target).empty();
|
||||
this.options.target.appendChild(this.svg_element);
|
||||
const div = $("<div />")
|
||||
.css("position", "absolute")
|
||||
.css("left", "0")
|
||||
.css("top", "0")
|
||||
.css("width", "100%")
|
||||
.css("padding-top", `${this.options.y_offset / 2}px`);
|
||||
this.commits_list_element = div[0];
|
||||
this.options.target.appendChild(this.commits_list_element);
|
||||
const p = $("<p />")
|
||||
.css("height", `${this.options.y_offset}px`)
|
||||
.css("display", "block")
|
||||
.css("padding", "0")
|
||||
.css("margin", "0")
|
||||
.css("line-height", `${this.options.y_offset}px`)
|
||||
.css("vertical-align", "middle");
|
||||
p.addClass("git_grapth_load_more");
|
||||
p.on("click", (e) => this.render_next());
|
||||
p.text(__("More").__());
|
||||
this.load_more_el = p[0];
|
||||
this.commits_list_element.appendChild(this.load_more_el);
|
||||
const popup = $("<div />")
|
||||
.css("position", "absolute")
|
||||
.css("top", "0")
|
||||
.css("height", this.options.popup_height + "px")
|
||||
.css("display", "none")
|
||||
.css("user-select", "text")
|
||||
.addClass("git_grapth_commit_detail");
|
||||
this.commit_detail_el = popup[0];
|
||||
this.options.target.appendChild(this.commit_detail_el);
|
||||
}
|
||||
render_next() {
|
||||
this.load(this.oldest_commit_date)
|
||||
.then((data) => {
|
||||
if (this.oldest_commit_date) {
|
||||
// remove the first commit as it is already in
|
||||
// the graph
|
||||
data.shift();
|
||||
}
|
||||
this.draw_graph(data);
|
||||
})
|
||||
.catch(e => this.error(e));
|
||||
}
|
||||
make_svg_el(tag, attrs) {
|
||||
const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
|
||||
for (var k in attrs)
|
||||
el.setAttribute(k, attrs[k]);
|
||||
return el;
|
||||
}
|
||||
max_line_off_x() {
|
||||
if (this.lines_data.length == 0)
|
||||
return 0;
|
||||
return Math.max.apply(Math, this.lines_data.map((o) => o.x_offset));
|
||||
}
|
||||
update_line_data(commit, y_offset) {
|
||||
const parent_commits = commit.hashes.parents.split(" ");
|
||||
// get the list of child lines
|
||||
const children = this.lines_data.filter((line) => line.next_commit == commit.hashes.commit);
|
||||
let merge = {
|
||||
src: [],
|
||||
dest: undefined
|
||||
};
|
||||
if (children.length === 0) {
|
||||
// add new line
|
||||
let line = {
|
||||
next_commit: parent_commits[0],
|
||||
x_offset: this.max_line_off_x() + this.options.x_offset,
|
||||
current_commit: commit.hashes.commit,
|
||||
beginning: true,
|
||||
y_offset: y_offset,
|
||||
color: this.gen_color(this.lines_data.length),
|
||||
};
|
||||
this.lines_data.push(line);
|
||||
merge.dest = line;
|
||||
}
|
||||
else {
|
||||
let min_offset_x = Math.min.apply(Math, children.map((o) => o.x_offset));
|
||||
let line = undefined;
|
||||
for (let el of children) {
|
||||
if (el.x_offset == min_offset_x) {
|
||||
line = el;
|
||||
line.next_commit = parent_commits[0];
|
||||
line.current_commit = commit.hashes.commit;
|
||||
line.y_offset = y_offset;
|
||||
}
|
||||
else {
|
||||
this.lines_data.splice(this.lines_data.indexOf(el), 1);
|
||||
merge.src.push(el);
|
||||
}
|
||||
}
|
||||
merge.dest = line;
|
||||
}
|
||||
if (parent_commits.length === 2) {
|
||||
let line = undefined;
|
||||
line = this.lines_data.filter(l => l.next_commit == parent_commits[1])[0];
|
||||
if (!line) {
|
||||
// add new line
|
||||
line = {
|
||||
next_commit: parent_commits[1],
|
||||
x_offset: this.max_line_off_x() + this.options.x_offset,
|
||||
current_commit: commit.hashes.commit,
|
||||
beginning: true,
|
||||
y_offset: y_offset + this.options.y_offset,
|
||||
color: this.gen_color(this.lines_data.length),
|
||||
};
|
||||
this.lines_data.push(line);
|
||||
}
|
||||
else {
|
||||
line.y_offset = y_offset + this.options.y_offset;
|
||||
}
|
||||
merge.src.push(line);
|
||||
}
|
||||
return merge;
|
||||
}
|
||||
draw_line(_x1, _y1, _x2, _y2, color, stroke) {
|
||||
let line_opt = {
|
||||
stroke: color,
|
||||
fill: 'none',
|
||||
"stroke-width": 1.5
|
||||
};
|
||||
if (stroke) {
|
||||
line_opt['stroke-width'] = stroke;
|
||||
}
|
||||
if (_x1 == _x2) {
|
||||
line_opt.d = `M ${_x1},${_y1} L ${_x2},${_y2}`;
|
||||
}
|
||||
else {
|
||||
let x1 = _x1;
|
||||
let y1 = _y1;
|
||||
let x2 = _x2;
|
||||
let y2 = _y2;
|
||||
let dx = Math.abs(x2 - x1);
|
||||
let dy = Math.abs(y2 - y1);
|
||||
if (_y1 < _y2) {
|
||||
x1 = _x2;
|
||||
y1 = _y2;
|
||||
x2 = _x1;
|
||||
y2 = _y1;
|
||||
}
|
||||
line_opt.d = `M ${x1},${y1} C ${x1},${y1 - dy} ${x2},${y2 + dy} ${x2},${y2}`;
|
||||
}
|
||||
const line = this.make_svg_el("path", line_opt);
|
||||
return line;
|
||||
}
|
||||
gen_commit_data_header(name, value) {
|
||||
const p = $("<p />")
|
||||
.css("display", "block");
|
||||
p[0].innerHTML = `<b>${name.__()}</b>: ${value}`;
|
||||
return p[0];
|
||||
}
|
||||
open_popup(commit) {
|
||||
const el = commit.domel;
|
||||
if (!el)
|
||||
return;
|
||||
$(this.commit_detail_el).empty();
|
||||
const position = $(el).position();
|
||||
const bbox = this.svg_element.getBBox();
|
||||
const off_left = bbox.x + bbox.width + this.options.x_offset / 2;
|
||||
const svg = this.make_svg_el("svg", {
|
||||
width: off_left - commit.cx + 5,
|
||||
height: this.options.y_offset
|
||||
});
|
||||
$(svg)
|
||||
.css("display", "block")
|
||||
.css("position", "absolute")
|
||||
.css("left", "0")
|
||||
.css("top", "-2px");
|
||||
svg.appendChild(this.draw_line(0, this.options.y_offset / 2, off_left - commit.cx, this.options.y_offset / 2, commit.color));
|
||||
/*
|
||||
svg.appendChild(
|
||||
this.make_svg_el("circle",{
|
||||
cx: off_left-commit.cx - 1,
|
||||
cy: this.options.y_offset/2,
|
||||
r:4,
|
||||
fill: commit.color,
|
||||
"stroke-width": 0.0
|
||||
})
|
||||
);*/
|
||||
$(this.commit_detail_el)
|
||||
.css("border", "2px solid " + commit.color)
|
||||
.css("color", commit.color)
|
||||
.append($("<div />")
|
||||
.css("position", "absolute")
|
||||
.css("height", this.options.y_offset)
|
||||
.css("left", commit.cx - off_left)
|
||||
.css("padding-left", off_left - commit.cx)
|
||||
.append(svg)
|
||||
.append($("<i/>").text('[X]')
|
||||
.css("cursor", "pointer"))
|
||||
.addClass("git_grapth_commit_detail_ctrl")
|
||||
.on("click", (e) => {
|
||||
$(this.commit_detail_el)
|
||||
.css("display", "none")
|
||||
.empty();
|
||||
}));
|
||||
const left = $("<div />")
|
||||
.css("display", "block")
|
||||
.css("overflow-y", "auto")
|
||||
.css("overflow-x", "hidden")
|
||||
.css("flex", "1")
|
||||
.css("border-right", "1px solid " + commit.color)
|
||||
.addClass("git_grapth_commit_detail_left");
|
||||
const right = $("<div />")
|
||||
.css("display", "block")
|
||||
.css("overflow-y", "auto")
|
||||
.css("overflow-x", "hidden")
|
||||
.css("flex", "1")
|
||||
.addClass("git_grapth_commit_detail_right");
|
||||
// display
|
||||
left.append(this.gen_commit_data_header(__("Commit"), commit.hashes.commit));
|
||||
left.append(this.gen_commit_data_header(__("Parents"), commit.hashes.parents));
|
||||
left.append(this.gen_commit_data_header(__("Author"), `${commit.committer.name} <${commit.committer.email}>`));
|
||||
left.append(this.gen_commit_data_header(__("Date"), (new Date(commit.committer.date).toDateString())));
|
||||
left.append($("<pre />")
|
||||
.css("white-space", "pre-wrap")
|
||||
.text(commit.message));
|
||||
this.commit_detail_el.appendChild(left[0]);
|
||||
this.commit_detail_el.appendChild(right[0]);
|
||||
this.list_file(commit.hashes.commit)
|
||||
.then((files) => {
|
||||
const ul = $('<ul/>');
|
||||
$.each(files, (index, value) => {
|
||||
const arr = value.split("\t");
|
||||
const li = $('<li/>');
|
||||
const a = $('<a/>')
|
||||
.css("cursor", "pointer")
|
||||
.addClass(`git_graph_file_${arr[0].toLowerCase()}`)
|
||||
.on("click", e => {
|
||||
if (this._on_open_diff) {
|
||||
Promise.all([
|
||||
this.get_file(arr[1], `${commit.hashes.commit}^`),
|
||||
this.get_file(arr[1], commit.hashes.commit)
|
||||
])
|
||||
.then((values) => {
|
||||
// create the file
|
||||
const files = values.map((content, index) => {
|
||||
const file = `mem://${commit.hashes.commit.slice(0, 8)}${index == 0 ? "^" : ""}/${arr[1]}`.asFileHandle();
|
||||
file.cache = content;
|
||||
file.info.mime = "text/plain";
|
||||
return file;
|
||||
});
|
||||
this._on_open_diff(files);
|
||||
})
|
||||
.catch((e) => {
|
||||
OS.announcer.oserror(__("Unable to fetch diff of {0}: {1}", commit.hashes.commit, e.toString()), e);
|
||||
});
|
||||
}
|
||||
});
|
||||
a.text(arr[1]);
|
||||
ul.append(li.append(a));
|
||||
});
|
||||
right.append(ul);
|
||||
})
|
||||
.catch((e) => OS.announcer.oserror(__("Unable to get commit changes: {0}", e.toString()), e));
|
||||
// scroll down if necessary
|
||||
$(this.commit_detail_el)
|
||||
.css("top", position.top)
|
||||
.css("left", off_left)
|
||||
.css("display", "flex")
|
||||
.css("width", `calc(100% - ${off_left + this.options.x_offset}px)`)
|
||||
.css("fflex-direction", "row");
|
||||
const delta = this.commit_detail_el.getBoundingClientRect().bottom -
|
||||
this.options.target.getBoundingClientRect().bottom;
|
||||
if (delta > 0) {
|
||||
this.options.target.scrollTop += delta + 10;
|
||||
}
|
||||
}
|
||||
render_commit(commit, color) {
|
||||
let current = false;
|
||||
const p = $("<p />")
|
||||
.css("padding", "0")
|
||||
.css("margin", "0")
|
||||
.css("display", "block")
|
||||
.css("height", `${this.options.y_offset}px`)
|
||||
.css("line-height", `${this.options.y_offset}px`)
|
||||
.css("color", color)
|
||||
.css("vertical-align", "middle")
|
||||
.css("white-space", "nowrap")
|
||||
.css("overflow", "hidden");
|
||||
p.addClass("git_graph_commit_message");
|
||||
let html = `<i class="git_graph_commit_hash">${commit.hashes.commit.slice(0, 8)}</i> `;
|
||||
commit.branches = [];
|
||||
for (const tag of commit.extra.split(",").map(e => e.trim()).filter(e => e != "")) {
|
||||
let found = tag.match(/HEAD -> (.*)/i);
|
||||
if (found && found.length == 2) {
|
||||
html += `<i class = "git_graph_commit_branch">${found[1]}</i> `;
|
||||
p.addClass("git_graph_commit_current_head");
|
||||
current = true;
|
||||
commit.branches.push(found[1]);
|
||||
this.current_head = commit;
|
||||
}
|
||||
else if ((found = tag.match(/tag: (.*)/i))) {
|
||||
html += `<i class = "git_graph_commit_tag">${found[1]}</i> `;
|
||||
}
|
||||
else {
|
||||
html += `<i class = "git_graph_commit_branch">${tag}</i> `;
|
||||
commit.branches.push(tag);
|
||||
}
|
||||
}
|
||||
html += `<i class ="git_graph_commit_text">${commit.message.split("\n")[0]}</i> `;
|
||||
html += `<i class ="git_graph_commit_author">${commit.committer.name}</i> `;
|
||||
html += `<i class ="git_graph_commit_date">${new Date(commit.committer.date).toDateString()}</i>`;
|
||||
p[0].innerHTML = html;
|
||||
p.on("click", (e) => {
|
||||
this.open_popup(commit);
|
||||
});
|
||||
this.commits_list_element.insertBefore(p[0], this.load_more_el);
|
||||
commit.domel = p[0];
|
||||
return current;
|
||||
}
|
||||
draw_graph(data) {
|
||||
for (const commit of data) {
|
||||
this.oldest_commit_date = commit.committer.date;
|
||||
if (commit.extra.includes("refs/stash")) {
|
||||
continue;
|
||||
}
|
||||
this.commits[commit.hashes.commit] = commit;
|
||||
let merge = this.update_line_data(commit, this.current_y_offset);
|
||||
let lines = merge.src;
|
||||
// combine the lines
|
||||
for (const linedata of lines.filter(o => o.x_offset != merge.dest.x_offset)) {
|
||||
this.svg_element.appendChild(this.draw_line(merge.dest.x_offset, merge.dest.y_offset, linedata.x_offset, linedata.y_offset, linedata.color));
|
||||
}
|
||||
this.lines_data.sort((a, b) => a.x_offset - b.x_offset);
|
||||
let x_offset = this.options.x_offset;
|
||||
for (const linedata of this.lines_data) {
|
||||
if (linedata.beginning && linedata.y_offset > this.current_y_offset) {
|
||||
continue;
|
||||
}
|
||||
linedata.y_offset = this.current_y_offset;
|
||||
if (!linedata.beginning) {
|
||||
if (linedata.x_offset > x_offset) {
|
||||
this.svg_element.appendChild(this.draw_line(linedata.x_offset, linedata.y_offset - this.options.y_offset, linedata.x_offset - this.options.x_offset, linedata.y_offset, linedata.color));
|
||||
linedata.x_offset = x_offset;
|
||||
}
|
||||
else {
|
||||
this.svg_element.appendChild(this.draw_line(linedata.x_offset, linedata.y_offset - this.options.y_offset, linedata.x_offset, linedata.y_offset, linedata.color));
|
||||
}
|
||||
}
|
||||
x_offset += this.options.x_offset;
|
||||
linedata.beginning = false;
|
||||
}
|
||||
let options = {
|
||||
cx: merge.dest.x_offset,
|
||||
cy: merge.dest.y_offset,
|
||||
r: 4,
|
||||
fill: merge.dest.color,
|
||||
stroke: 'black',
|
||||
"stroke-width": 0.0
|
||||
};
|
||||
if (this.render_commit(commit, merge.dest.color)) {
|
||||
options.r = options.r * 1.5;
|
||||
}
|
||||
commit.cx = options.cx;
|
||||
commit.color = options.fill;
|
||||
const circle = this.make_svg_el("circle", options);
|
||||
this.svg_element.appendChild(circle);
|
||||
this.current_y_offset += this.options.y_offset;
|
||||
}
|
||||
const bbox = this.svg_element.getBBox();
|
||||
this.svg_element.setAttribute("width", (bbox.x + bbox.width).toString());
|
||||
this.svg_element.setAttribute("height", (bbox.y + bbox.height + this.options.y_offset / 2).toString());
|
||||
//$(this.commits_list_element).css("left", `-${bbox.x + bbox.width}px`);
|
||||
$(".git_graph_commit_message", this.commits_list_element).css("padding-left", `${bbox.x + bbox.width + this.options.x_offset / 2}px`);
|
||||
}
|
||||
list_file(hash) {
|
||||
let request = {
|
||||
action: 'list_file',
|
||||
args: {
|
||||
commit: hash
|
||||
}
|
||||
};
|
||||
return this.call(request);
|
||||
}
|
||||
get_changes(file, hash) {
|
||||
let request = {
|
||||
action: 'get_changes',
|
||||
args: {
|
||||
commit: hash,
|
||||
file: file
|
||||
}
|
||||
};
|
||||
return this.call(request);
|
||||
}
|
||||
get_file(file, hash) {
|
||||
let request = {
|
||||
action: 'get_file',
|
||||
args: {
|
||||
commit: hash,
|
||||
file: file
|
||||
}
|
||||
};
|
||||
return this.call(request);
|
||||
}
|
||||
}
|
||||
API.LibGitGraph = LibGitGraph;
|
||||
})(API = OS.API || (OS.API = {}));
|
||||
})(OS || (OS = {}));
|
16
GitGrapth/build/debug/package.json
Normal file
16
GitGrapth/build/debug/package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"pkgname": "GitGraph",
|
||||
"app":"GitGraph",
|
||||
"name":"GIT Visualization",
|
||||
"description":"Git Grapth",
|
||||
"info":{
|
||||
"author": "",
|
||||
"email": ""
|
||||
},
|
||||
"version":"0.0.1-a",
|
||||
"category":"Other",
|
||||
"iconclass":"fa fa-adn",
|
||||
"mimes":["none"],
|
||||
"dependencies":[],
|
||||
"locale": {}
|
||||
}
|
8
GitGrapth/build/debug/scheme.html
Normal file
8
GitGrapth/build/debug/scheme.html
Normal file
@ -0,0 +1,8 @@
|
||||
<afx-app-window apptitle="LibGitGraph" width="600" height="400" data-id="GitGraph">
|
||||
<afx-hbox >
|
||||
<div data-id="git-graph">
|
||||
</div>
|
||||
<!--afx-resizer data-width="3"></afx-resizer>
|
||||
<div data-id="commit-detail"></div-->
|
||||
</afx-hbox>
|
||||
</afx-app-window>
|
201
GitGrapth/main.css
Normal file
201
GitGrapth/main.css
Normal file
@ -0,0 +1,201 @@
|
||||
afx-app-window[data-id="GitGraph"] div[data-id="git-graph"]
|
||||
{
|
||||
overflow-y: auto;
|
||||
background-color: bisque;
|
||||
}
|
||||
|
||||
p.git_graph_commit_message:hover
|
||||
{
|
||||
background-color:rgba(39, 39, 39,0.5);
|
||||
}
|
||||
p.git_graph_commit_current_head
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p.git_graph_commit_message i.git_graph_commit_hash
|
||||
{
|
||||
font-weight: bold;
|
||||
margin-right: 5px;
|
||||
font-style: normal;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
p.git_graph_commit_message i.git_graph_commit_tag,
|
||||
p.git_graph_commit_message i.git_graph_commit_branch
|
||||
{
|
||||
border: 1px solid;
|
||||
font-style: normal;
|
||||
border-radius: 5px;
|
||||
line-height: 20px;
|
||||
padding: 0;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
p.git_graph_commit_message i.git_graph_commit_tag::before
|
||||
{
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F5AF";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
|
||||
p.git_graph_commit_message i.git_graph_commit_branch::before
|
||||
{
|
||||
font-family: 'FontAwesome';
|
||||
content: "\f126";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
|
||||
div.git_grapth_commit_detail
|
||||
{
|
||||
border-radius: 5px;
|
||||
background-color: whitesmoke;
|
||||
box-shadow: 0px 3px 6px 0px rgb(0,0,0,0.5);
|
||||
}
|
||||
|
||||
div.git_grapth_commit_detail_right,
|
||||
div.git_grapth_commit_detail_left {
|
||||
background-color: transparent !important;
|
||||
padding: 10px;
|
||||
}
|
||||
div.git_grapth_commit_detail_left {
|
||||
padding-top: 20px;
|
||||
}
|
||||
div.git_grapth_commit_detail_left p{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
word-wrap:break-word;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
div.git_grapth_commit_detail_left pre
|
||||
{
|
||||
margin: 0;
|
||||
padding-top: 10px;
|
||||
display: block;
|
||||
border-top: 1px solid #9c9c9c;
|
||||
}
|
||||
div.git_grapth_commit_detail_right a{
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
|
||||
div.git_grapth_commit_detail_right a.git_graph_file_m::before
|
||||
{
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F364";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
color: blue !important;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
div.git_grapth_commit_detail_right a.git_graph_file_a::before
|
||||
{
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F37D";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
color: green !important;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
div.git_grapth_commit_detail_right a.git_graph_file_d::before
|
||||
{
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F368";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
color: red !important;
|
||||
padding-right: 5px;
|
||||
}
|
||||
div.git_grapth_commit_detail_ctrl i::before{
|
||||
display: block;
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F622";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
div.git_grapth_commit_detail_ctrl i {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
div.git_grapth_commit_detail_right ul{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
p.git_graph_commit_message i.git_graph_commit_text
|
||||
{
|
||||
font-style: normal;
|
||||
display: inline-block;
|
||||
}
|
||||
p.git_graph_commit_message i.git_graph_commit_author::before
|
||||
{
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F4DC";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
padding-left: 5px;
|
||||
padding-right: 3px;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
line-height: 20px;
|
||||
}
|
||||
p.git_graph_commit_message i.git_graph_commit_date::before
|
||||
{
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F205";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
padding-right: 3px;
|
||||
padding-left: 5px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
line-height: 20px;
|
||||
}
|
||||
p.git_grapth_load_more
|
||||
{
|
||||
font-weight: normal ;
|
||||
text-align: left;
|
||||
color:#131313;
|
||||
display: inline-block !important;
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
|
||||
p.git_grapth_load_more::before
|
||||
{
|
||||
font-family: 'bootstrap-icons';
|
||||
content: "\F118";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
p.git_grapth_load_more:hover
|
||||
{
|
||||
cursor: pointer;
|
||||
}
|
44
GitGrapth/main.ts
Normal file
44
GitGrapth/main.ts
Normal file
@ -0,0 +1,44 @@
|
||||
namespace OS {
|
||||
export namespace API
|
||||
{
|
||||
export declare class LibGitGraph
|
||||
{
|
||||
constructor(options:GenericObject<any>);
|
||||
base_dir: VFS.BaseFileHandle;
|
||||
on_open_diff: (file:VFS.BaseFileHandle[]) => void;
|
||||
list_file: (commit: string) => Promise<string[]>;
|
||||
get_changes: (file:string, commit: string) => Promise<string>;
|
||||
get_file: (file:string, commit: string) => Promise<string>
|
||||
}
|
||||
}
|
||||
export namespace application {
|
||||
/**
|
||||
*
|
||||
* @class GitGraph
|
||||
* @extends {BaseApplication}
|
||||
*/
|
||||
export class GitGraph extends BaseApplication {
|
||||
constructor(args: AppArgumentsType[]) {
|
||||
super("GitGraph", args);
|
||||
}
|
||||
main(): void {
|
||||
let graph = new API.LibGitGraph({
|
||||
target: this.find("git-graph")
|
||||
});
|
||||
graph.on_open_diff = (files) => {
|
||||
console.log(files);
|
||||
(OS.PM.processes.Antedit[0] as any).openDiff(files)
|
||||
/*
|
||||
this._gui.launch("Antedit", [])
|
||||
.then((p) =>{
|
||||
p.observable.one("rendered",() =>(p as any).openDiff(files));
|
||||
});
|
||||
*/
|
||||
}
|
||||
graph.base_dir = "home://workspace/antos/".asFileHandle();
|
||||
}
|
||||
}
|
||||
|
||||
//GitGraph.dependencies = ["pkg://GitGraph/libgitgraph.js"];
|
||||
}
|
||||
}
|
16
GitGrapth/package.json
Normal file
16
GitGrapth/package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"pkgname": "GitGraph",
|
||||
"app":"GitGraph",
|
||||
"name":"GIT Visualization",
|
||||
"description":"Git Grapth",
|
||||
"info":{
|
||||
"author": "",
|
||||
"email": ""
|
||||
},
|
||||
"version":"0.0.1-a",
|
||||
"category":"Other",
|
||||
"iconclass":"fa fa-adn",
|
||||
"mimes":["none"],
|
||||
"dependencies":[],
|
||||
"locale": {}
|
||||
}
|
8
GitGrapth/scheme.html
Normal file
8
GitGrapth/scheme.html
Normal file
@ -0,0 +1,8 @@
|
||||
<afx-app-window apptitle="LibGitGraph" width="600" height="400" data-id="GitGraph">
|
||||
<afx-hbox >
|
||||
<div data-id="git-graph">
|
||||
</div>
|
||||
<!--afx-resizer data-width="3"></afx-resizer>
|
||||
<div data-id="commit-detail"></div-->
|
||||
</afx-hbox>
|
||||
</afx-app-window>
|
Loading…
Reference in New Issue
Block a user