/*
* This file is part of gitg
*
* Copyright (C) 2012 - Jesse van den Kieboom
*
* gitg is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* gitg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with gitg. If not, see .
*/
namespace Gitg
{
public class Lanes : Object
{
public int inactive_max { get; set; default = 30; }
public int inactive_collapse { get; set; default = 10; }
public int inactive_gap { get; set; default = 10; }
public bool inactive_enabled { get; set; default = true; }
private SList d_previous;
private Gee.LinkedList d_lanes;
private HashTable d_collapsed;
private Gee.HashSet? d_roots;
class LaneContainer
{
public Lane lane;
public int inactive;
public Ggit.OId? from;
public Ggit.OId? to;
public LaneContainer.with_color(Ggit.OId? from,
Ggit.OId? to,
Color? color)
{
this.from = from;
this.to = to;
this.lane = new Lane.with_color(color);
this.inactive = 0;
}
public LaneContainer(Ggit.OId? from,
Ggit.OId? to)
{
this.with_color(from, to, null);
}
public void next(int index)
{
var hidden = is_hidden;
lane = lane.copy();
lane.tag = LaneTag.NONE;
lane.from = new SList();
if (!hidden)
{
lane.from.prepend(index);
}
is_hidden = hidden;
if (to != null && inactive >= 0)
{
++inactive;
}
}
public bool is_hidden
{
get { return (lane.tag & LaneTag.HIDDEN) != 0; }
set
{
if (value)
{
lane.tag |= LaneTag.HIDDEN;
}
else
{
lane.tag &= ~LaneTag.HIDDEN;
}
}
}
}
[Compact]
class CollapsedLane
{
public Color color;
public uint index;
public Ggit.OId? from;
public Ggit.OId? to;
public CollapsedLane(LaneContainer container)
{
color = container.lane.color;
from = container.from;
to = container.to;
}
}
public Lanes()
{
d_collapsed = new HashTable(Ggit.OId.hash,
Ggit.OId.equal);
var settings = new Settings("org.gnome.gitg.preferences.history");
settings.bind("collapse-inactive-lanes-enabled",
this,
"inactive-enabled",
SettingsBindFlags.GET | SettingsBindFlags.SET);
settings.bind("collapse-inactive-lanes",
this,
"inactive-collapse",
SettingsBindFlags.GET | SettingsBindFlags.SET);
reset();
}
public void reset(Ggit.OId[]? reserved = null,
Gee.HashSet? roots = null)
{
d_lanes = new Gee.LinkedList();
d_roots = roots;
Color.reset();
if (reserved != null)
{
foreach (var r in reserved)
{
var ct = new LaneContainer(null, r);
ct.inactive = -1;
ct.is_hidden = true;
d_lanes.add(ct);
}
}
d_collapsed.remove_all();
d_previous = new SList();
}
public bool next(Commit next,
out SList lanes,
out int nextpos)
{
var myoid = next.get_id();
if (inactive_enabled)
{
collapse_lanes();
expand_lanes(next);
}
LaneContainer? mylane = find_lane_by_oid(myoid, out nextpos);
if (mylane == null && d_roots != null && !d_roots.contains(myoid))
{
lanes = null;
return false;
}
if (mylane == null)
{
// there is no lane reserved for this commit, add a new lane
mylane = new LaneContainer(myoid, null);
d_lanes.add(mylane);
nextpos = (int)d_lanes.size - 1;
}
else
{
// copy the color here because the commit is a new stop
mylane.lane.color = mylane.lane.color.copy();
mylane.to = null;
mylane.from = next.get_id();
if (mylane.is_hidden && d_roots != null && d_roots.contains(myoid))
{
mylane.is_hidden = false;
mylane.lane.from = new SList();
}
if (mylane.inactive >= 0)
{
mylane.inactive = 0;
}
}
var hidden = mylane.is_hidden;
lanes = lanes_list();
prepare_lanes(next, nextpos, hidden);
return !hidden;
}
private void prepare_lanes(Commit next, int pos, bool hidden)
{
var parents = next.get_parents();
var myoid = next.get_id();
if (!hidden)
{
init_next_layer();
}
var mylane = d_lanes[pos];
for (uint i = 0; i < parents.size; ++i)
{
int lnpos;
var poid = parents.get_id(i);
var container = find_lane_by_oid(poid, out lnpos);
if (container != null)
{
// there is already a lane for this parent. This means that
// we add pos as a merge for the lane.
if (i == 0 && pos < lnpos)
{
// we are at the mainline of a merge, and this parent has
// already been assigned to an existing lane, if our
// lane's pos is smaller, then the this parent should be in
// our lane instead.
mylane.to = poid;
mylane.from = myoid;
if (!container.is_hidden)
{
mylane.lane.from.append(lnpos);
mylane.is_hidden = false;
}
mylane.lane.color = mylane.lane.color.copy();
if (mylane.inactive >= 0)
{
mylane.inactive = 0;
}
d_lanes.remove(container);
}
else
{
container.from = myoid;
if (!hidden)
{
container.lane.from.append(pos);
}
container.lane.color = container.lane.color.copy();
if (!hidden)
{
container.is_hidden = false;
}
if (container.inactive >= 0)
{
container.inactive = 0;
}
}
continue;
}
else if (mylane != null && mylane.to == null)
{
// there is no parent yet which can proceed on the current
// commit lane, so set it now
mylane.to = poid;
mylane.lane.color = mylane.lane.color.copy();
}
else if (!hidden)
{
// generate a new lane for this parent
var newlane = new LaneContainer(myoid, poid);
newlane.lane.from.prepend(pos);
d_lanes.add(newlane);
}
}
if (mylane != null && mylane.to == null)
{
// remove current lane if no longer needed (i.e. merged)
d_lanes.remove(mylane);
}
// store new commit in track list
if (d_previous.length() == inactive_collapse + inactive_gap + 1)
{
d_previous.delete_link(d_previous.last());
}
d_previous.prepend(next);
}
private void add_collapsed(LaneContainer container,
int index)
{
var collapsed = new CollapsedLane(container);
collapsed.index = index;
d_collapsed.insert(container.to, (owned)collapsed);
}
private void collapse_lane(LaneContainer container,
int index)
{
add_collapsed(container, index);
unowned SList item = d_previous;
while (item != null)
{
var commit = item.data;
unowned SList lns = commit.get_lanes();
if (lns != null)
{
unowned Lane lane = lns.nth_data(index);
if (item.next != null)
{
var newindex = lane.from.data;
lns = commit.remove_lane(lane);
if (item.next.next != null)
{
update_merge_indices(lns, newindex, -1);
}
var mylane = commit.mylane;
if (mylane > index)
{
--commit.mylane;
}
index = newindex;
}
else
{
lane.tag |= LaneTag.END;
lane.boundary_id = container.to;
}
}
item = item.next;
}
}
private void collapse_lanes()
{
int index = 0;
var iter = d_lanes.iterator();
while (iter.next())
{
var container = iter.get();
if (container.inactive != inactive_max + inactive_gap)
{
++index;
continue;
}
collapse_lane(container, container.lane.from.data);
update_current_lane_merge_indices(index, -1);
iter.remove();
}
}
private int ensure_correct_index(Commit commit,
int index)
{
var len = commit.get_lanes().length();
if (index > len)
{
return (int)len;
}
else
{
return index;
}
}
private void update_lane_merge_indices(SList from,
int index,
int direction)
{
while (from != null)
{
int idx = from.data;
if (idx > index || (direction > 0 && idx == index))
{
from.data = idx + direction;
}
from = from.next;
}
}
private void update_merge_indices(SList lanes,
int index,
int direction)
{
foreach (unowned Lane lane in lanes)
{
update_lane_merge_indices(lane.from, index, direction);
}
}
private void update_current_lane_merge_indices(int index,
int direction)
{
foreach (var container in d_lanes)
{
update_lane_merge_indices(container.lane.from,
index,
direction);
}
}
private void expand_lane(CollapsedLane lane)
{
var index = lane.index;
var ln = new Lane.with_color(lane.color);
var len = d_lanes.size;
if (index > len)
{
index = len;
}
var next = ensure_correct_index(d_previous.data, (int)index);
var container = new LaneContainer.with_color(lane.from,
lane.to,
lane.color);
update_current_lane_merge_indices((int)index, 1);
container.lane.from.prepend(next);
d_lanes.insert((int)index, container);
index = next;
uint cnt = 0;
unowned SList ptr = d_previous;
while (ptr != null)
{
var commit = ptr.data;
if (cnt == inactive_collapse)
{
break;
}
// Insert new lane at the index
Lane copy = ln.copy();
unowned SList lns = commit.get_lanes();
if (ptr.next == null || cnt + 1 == inactive_collapse)
{
copy.boundary_id = lane.from;
copy.tag |= LaneTag.START;
}
else
{
next = ensure_correct_index(ptr.next.data, (int)index);
copy.from.prepend(next);
update_merge_indices(lns, (int)index, 1);
}
commit.insert_lane(copy, (int)index);
var mylane = commit.mylane;
if (mylane >= index)
{
++commit.mylane;
}
index = next;
++cnt;
ptr = ptr.next;
}
}
private void expand_lane_from_oid(Ggit.OId id)
{
unowned CollapsedLane? collapsed = d_collapsed.lookup(id);
if (collapsed != null)
{
expand_lane(collapsed);
d_collapsed.remove(id);
}
}
private void expand_lanes(Commit commit)
{
expand_lane_from_oid(commit.get_id());
var parents = commit.get_parents();
for (uint i = 0; i < parents.size; ++i)
{
expand_lane_from_oid(parents.get_id(i));
}
}
private void init_next_layer()
{
int index = 0;
foreach (var container in d_lanes)
{
container.next(index++);
}
}
private LaneContainer? find_lane_by_oid(Ggit.OId id,
out int pos)
{
int p = 0;
foreach (var container in d_lanes)
{
if (container != null &&
id.equal(container.to))
{
pos = p;
return container;
}
++p;
}
pos = -1;
return null;
}
private SList lanes_list()
{
var ret = new SList();
foreach (var container in d_lanes)
{
ret.prepend(container.lane.copy());
}
ret.reverse();
return ret;
}
}
}
// ex:set ts=4 noet