diff --git a/data/gource.1 b/data/gource.1 index e46b22aa..633e4b91 100644 --- a/data/gource.1 +++ b/data/gource.1 @@ -105,6 +105,24 @@ Time in seconds files remain idle before they are removed or 0 for no limit. \fB\-\-file\-idle\-time\-at\-end SECONDS\fR Time in seconds files remain idle at the end before they are removed. .TP +\fB\-S, \-\-scale-by-file-size\fR +Scale file nodes by file size. +.TP +\fB\-\-file-scale FACTOR\fR +Scale factor for file nodes (default: 1.0). +.TP +\fB\-\-dir-spacing FACTOR\fR +Spacing for directory nodes (default: 1.0). +.TP +\fB\-\-file-gravity FACTOR\fR +Gravity for file nodes (default: 0.000001). +.TP +\fB\-\-file-repulsion FACTOR\fR +Repulsion for file nodes (default: 1000.0). +.TP +\fB\-\-show-file-size-on-hover\fR +Show file size on hover. +.TP \fB\-e, \-\-elasticity FLOAT\fR Elasticity of nodes. .TP @@ -378,6 +396,8 @@ type - Single character for the update type - (A)dded, (M)odified or (D)ele file - Path of the file updated. .ti 10 colour - A colour for the file in hex (FFFFFF) format. Optional. +.ti 10 +file_size - The size of the file in bytes. Optional. .SS Caption Log Format diff --git a/gource.pro b/gource.pro index 9ce74162..cfa84383 100644 --- a/gource.pro +++ b/gource.pro @@ -23,7 +23,7 @@ mingw { INCLUDEPATH += C:\msys64\mingw64\include\freetype2 LIBS += -lmingw32 -lSDL2main -lSDL2.dll - LIBS += -lSDL2_image.dll -lfreetype.dll -lpcre2-8.dll -lpng.dll -lglew32.dll -lboost_system-mt -lboost_filesystem-mt -lopengl32 -lglu32 + LIBS += -lSDL2_image.dll -lfreetype.dll -lpcre2-8.dll -lpng.dll -lglew32.dll -lboost_filesystem-mt -lopengl32 -lglu32 LIBS += -static-libgcc -static-libstdc++ LIBS += -lcomdlg32 } diff --git a/src/dirnode.cpp b/src/dirnode.cpp index 6d23f385..a20a2e1d 100644 --- a/src/dirnode.cpp +++ b/src/dirnode.cpp @@ -16,11 +16,11 @@ */ #include "dirnode.h" +#include "gource_settings.h" float gGourceMinDirSize = 15.0; float gGourceForceGravity = 10.0; -float gGourceDirPadding = 1.5; bool gGourceNodeDebug = false; bool gGourceGravity = true; @@ -31,6 +31,33 @@ int gGourceFileInnerLoops = 0; std::map gGourceDirMap; +namespace { + +float hashUnit(unsigned int h) { + return (float) (h & 0x00ffffffu) / 16777215.0f; +} + +// Deterministic fallback direction used when two files overlap at exactly the same position. +vec2 separationFallbackDir(RFile* f1, RFile* f2) { + unsigned int h = (unsigned int) f1->getTagID() * 73856093u + ^ (unsigned int) f2->getTagID() * 19349663u + ^ 0x9e3779b9u; + float angle = (float) (h % 6283) / 1000.0f; // [0, 2pi) + return vec2(cosf(angle), sinf(angle)); +} + +vec2 smallClusterAnchor(RFile* f, float packed_radius) { + unsigned int h1 = (unsigned int) f->getTagID() * 2654435761u; + unsigned int h2 = h1 ^ 0x9e3779b9u ^ (h1 >> 16); + + float angle = hashUnit(h1) * PI * 2.0f; + float radial = sqrtf(hashUnit(h2)); + + return vec2(cosf(angle), sinf(angle)) * (packed_radius * radial); +} + +} + RDirNode::RDirNode(RDirNode* parent, const std::string & abspath) { changePath(abspath); @@ -108,7 +135,8 @@ void RDirNode::nodeUpdated(bool userInitiated) { if(userInitiated) since_last_node_change = 0.0; calcRadius(); - updateFilePositions(); + if (!gGourceSettings.scale_by_file_size) + updateFilePositions(); if(visible && noDirs() && noFiles()) visible = false; if(parent !=0) parent->nodeUpdated(true); } @@ -408,6 +436,19 @@ bool RDirNode::addFile(RFile* f) { //debugLog("addFile %s to %s\n", f->fullpath.c_str(), abspath.c_str()); files.push_back(f); + if(glm::length2(f->getPos()) < 0.000001f) { + // Seed new files around the center to avoid symmetric collapse into line-like layouts. + size_t file_index = files.size(); + unsigned int seed = (unsigned int) f->getTagID() * 2246822519u + ^ (unsigned int) file_index * 3266489917u + ^ 0x85ebca6bu; + float angle = hashUnit(seed) * PI * 2.0f; + float radial = sqrtf(hashUnit(seed ^ 0xc2b2ae35u)); + float base = std::max(0.3f, f->getSize() * 0.35f); + float spread = (1.0f + sqrtf((float)file_index) * 0.28f) * base; + vec2 dir(cosf(angle), sinf(angle)); + f->setPos(dir * (spread * radial)); + } if(!f->isHidden()) visible_count++; f->setDir(this); @@ -573,7 +614,17 @@ float RDirNode::getArea() const{ void RDirNode::calcRadius() { - float total_file_area = file_area * visible_count; + float total_file_area = 0; + if (gGourceSettings.scale_by_file_size) { + for (RFile* f : files) { + if (!f->isHidden()) { + float r = f->getSize() / 2.0f; + total_file_area += r * r * PI; + } + } + } else { + total_file_area = file_area * visible_count; + } dir_area = total_file_area; @@ -586,11 +637,11 @@ void RDirNode::calcRadius() { // parent_circ += node->getRadiusSqrt(); } - this->dir_radius = std::max(1.0f, (float)sqrt(dir_area)) * gGourceDirPadding; + this->dir_radius = std::max(1.0f, (float)sqrt(dir_area)) * gGourceSettings.dir_spacing; //this->dir_radius_sqrt = sqrt(dir_radius); //dir_radius_sqrt is not used // this->parent_radius = std::max(1.0, parent_circ / PI); - this->parent_radius = std::max(1.0f, (float) sqrt(total_file_area) * gGourceDirPadding); + this->parent_radius = std::max(1.0f, (float) sqrt(total_file_area)) * gGourceSettings.dir_spacing; } float RDirNode::distanceToParent() const{ @@ -892,6 +943,188 @@ void RDirNode::calcEdges() { } } +void RDirNode::applyFilePhysics(float dt) { + if (files.empty() || !gGourceSettings.scale_by_file_size) return; + + std::vector visible_files; + visible_files.reserve(files.size()); + + for(std::list::iterator it = files.begin(); it != files.end(); ++it) { + RFile* f = *it; + if(!f->isHidden()) visible_files.push_back(f); + } + + if(visible_files.empty()) return; + + // Position-based solver: resolve overlaps first, then gently compress toward center. + // This gives dense, near-touching clusters with minimal wobble. + const size_t file_count = visible_files.size(); + const int iterations = std::min(28, std::max(8, (int)file_count / 12 + 8)); + const float sub_dt = dt / (float)iterations; + + const float contact_gap = 0.08f; + const float center_pull_speed = std::max(0.001f, gGourceSettings.file_gravity * 220.0f); + const float separation_stiffness = glm::clamp(gGourceSettings.file_repulsion * 0.0008f, 0.35f, 1.0f); + + float occupied_area = 0.0f; + for(size_t i = 0; i < file_count; ++i) { + float r = visible_files[i]->getSize() * 0.5f; + occupied_area += PI * r * r; + } + float packed_radius = sqrtf(std::max(0.0001f, occupied_area / (float)PI)); + bool use_small_cluster_shaping = file_count <= 40; + + for (int iter = 0; iter < iterations; ++iter) { + // Keep the cluster anchored around the directory center. + vec2 centroid(0.0f, 0.0f); + for(size_t i = 0; i < file_count; ++i) { + centroid += visible_files[i]->getPos(); + } + centroid *= (1.0f / (float)file_count); + + float recenter_strength = std::min(1.0f, sub_dt * 8.0f); + for(size_t i = 0; i < file_count; ++i) { + RFile* f = visible_files[i]; + f->setPos(f->getPos() - centroid * recenter_strength); + } + + if(use_small_cluster_shaping) { + // Small clusters easily lock into line/chain minima. + // A weak attraction to deterministic disk anchors keeps them round and stable. + float anchor_pull = std::min(0.38f, sub_dt * 18.0f); + for(size_t i = 0; i < file_count; ++i) { + RFile* f = visible_files[i]; + vec2 anchor = smallClusterAnchor(f, packed_radius); + vec2 p = f->getPos(); + f->setPos(p + (anchor - p) * anchor_pull); + } + } + + int iter_overlaps = 0; + + // Resolve overlaps and enforce near-touching spacing. + for(size_t i = 0; i < file_count; ++i) { + RFile* f1 = visible_files[i]; + float r1 = f1->getSize() * 0.5f; + vec2 p1 = f1->getPos(); + + for(size_t j = i + 1; j < file_count; ++j) { + RFile* f2 = visible_files[j]; + float r2 = f2->getSize() * 0.5f; + vec2 p2 = f2->getPos(); + + float desired = r1 + r2 + contact_gap; + vec2 d = p2 - p1; + float dist2 = glm::length2(d); + + if(dist2 >= desired * desired) continue; + + float dist = 0.0f; + vec2 dir; + + if(dist2 <= 0.000001f) { + dir = separationFallbackDir(f1, f2); + } else { + dist = sqrtf(dist2); + dir = d / dist; + } + + float overlap = desired - dist; + float correction = overlap * separation_stiffness; + + float inv_mass1 = 1.0f / std::max(1.0f, r1); + float inv_mass2 = 1.0f / std::max(1.0f, r2); + float inv_mass_sum = inv_mass1 + inv_mass2; + + if(inv_mass_sum <= 0.0f) continue; + + float move1 = correction * (inv_mass1 / inv_mass_sum); + float move2 = correction * (inv_mass2 / inv_mass_sum); + + p1 -= dir * move1; + p2 += dir * move2; + + f1->setPos(p1); + f2->setPos(p2); + + iter_overlaps++; + } + } + + // Radial compression to make nodes pack tighter. + // If many overlaps are still being resolved this iteration, back off compression. + float pull_step = center_pull_speed * sub_dt; + if(iter_overlaps > 0) pull_step *= 0.2f; + + for(size_t i = 0; i < file_count; ++i) { + RFile* f = visible_files[i]; + vec2 p = f->getPos(); + float dist = glm::length(p); + if(dist > 0.0001f) { + float step = std::min(dist, pull_step); + f->setPos(p - normalise(p) * step); + } + } + + // Converged enough for this frame. + if(iter_overlaps == 0 && iter > 6) break; + } + + // Final cleanup pass to remove residual overlaps. + for(int pass = 0; pass < 3; ++pass) { + bool had_overlap = false; + for(size_t i = 0; i < file_count; ++i) { + RFile* f1 = visible_files[i]; + float r1 = f1->getSize() * 0.5f; + vec2 p1 = f1->getPos(); + + for(size_t j = i + 1; j < file_count; ++j) { + RFile* f2 = visible_files[j]; + float r2 = f2->getSize() * 0.5f; + vec2 p2 = f2->getPos(); + + float desired = r1 + r2 + contact_gap; + vec2 d = p2 - p1; + float dist2 = glm::length2(d); + if(dist2 >= desired * desired) continue; + + float dist = 0.0f; + vec2 dir; + if(dist2 <= 0.000001f) { + dir = separationFallbackDir(f1, f2); + } else { + dist = sqrtf(dist2); + dir = d / dist; + } + + float overlap = desired - dist; + float inv_mass1 = 1.0f / std::max(1.0f, r1); + float inv_mass2 = 1.0f / std::max(1.0f, r2); + float inv_mass_sum = inv_mass1 + inv_mass2; + if(inv_mass_sum <= 0.0f) continue; + + float move1 = overlap * (inv_mass1 / inv_mass_sum); + float move2 = overlap * (inv_mass2 / inv_mass_sum); + + p1 -= dir * move1; + p2 += dir * move2; + f1->setPos(p1); + f2->setPos(p2); + had_overlap = true; + } + } + + if(!had_overlap) break; + } + + // We intentionally avoid carrying momentum for this solver to reduce wobble. + for(size_t i = 0; i < file_count; ++i) { + visible_files[i]->vel = vec2(0.0f, 0.0f); + visible_files[i]->accel = vec2(0.0f, 0.0f); + } + +} + void RDirNode::logic(float dt) { //move @@ -904,11 +1137,14 @@ void RDirNode::logic(float dt) { } //update files - for(std::list::iterator it = files.begin(); it!=files.end(); it++) { - RFile* f = *it; - - f->logic(dt); - } + if (gGourceSettings.scale_by_file_size) { + applyFilePhysics(dt); + } + for(std::list::iterator it = files.begin(); it!=files.end(); it++) { + RFile* f = *it; + + f->logic(dt); + } //update child nodes for(std::list::iterator it = children.begin(); it != children.end(); it++) { @@ -1187,4 +1423,3 @@ void RDirNode::updateQuadItemBounds() { //set bounds quadItemBounds.set(pos - radoffset, pos + radoffset); } - diff --git a/src/dirnode.h b/src/dirnode.h index 8a78782b..0465ec56 100644 --- a/src/dirnode.h +++ b/src/dirnode.h @@ -93,8 +93,9 @@ class RDirNode : public QuadItem { void updateSplinePoint(float dt); void move(float dt); - vec2 calcFileDest(int layer_no, int file_no); + vec2 calcFileDest(int max_files, int file_no); void updateFilePositions(); + void applyFilePhysics(float dt); void adjustDepth(); void adjustPath(); diff --git a/src/file.cpp b/src/file.cpp index 9f81f291..65e2f746 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -16,6 +16,10 @@ */ #include "file.h" +#include "gource_settings.h" +#include +#include +#include float gGourceFileDiameter = 8.0; @@ -24,10 +28,37 @@ std::vector gGourceRemovedFiles; FXFont file_selected_font; FXFont file_font; +static float getScaledFileNodeSize(unsigned int file_size) { + double interpolation = gGourceSettings.file_size_scale_interpolation; + interpolation = std::max(0.0, std::min(2.0, interpolation)); + + const double uniform_curve = 1.0; + const double log_curve = (file_size > 0) + ? log((double)file_size + 1.0) + : 1.0; + // Keep linear mode usable by scaling bytes to KiB before blending. + const double linear_curve = ((double)file_size / 1024.0) + 1.0; + + double blended_curve = 0.0; + if(interpolation <= 1.0) { + blended_curve = uniform_curve + (log_curve - uniform_curve) * interpolation; + } else { + double t = interpolation - 1.0; + blended_curve = log_curve + (linear_curve - log_curve) * t; + } + + return gGourceSettings.file_scale * (float)blended_curve * 2.0f; +} + RFile::RFile(const std::string & name, const vec3 & colour, const vec2 & pos, int tagid) : Pawn(name,pos,tagid) { hidden = true; size = gGourceFileDiameter * 1.05; radius = size * 0.5; + file_size = 0; + target_size = size; + size_transition_start = size; + size_transition_elapsed = 0.0f; + size_transition_duration = 0.5f; setGraphic(gGourceSettings.file_graphic); @@ -73,6 +104,29 @@ RFile::RFile(const std::string & name, const vec3 & colour, const vec2 & pos, in RFile::~RFile() { } +void RFile::setFileSize(unsigned int new_file_size) { + file_size = new_file_size; + + if (gGourceSettings.scale_by_file_size) { + float new_size = getScaledFileNodeSize(file_size); + + // Smoothly interpolate size changes (especially useful for modifies). + size_transition_start = size; + target_size = std::max(0.01f, new_size); + size_transition_elapsed = 0.0f; + + if(std::fabs(target_size - size_transition_start) < 0.001f) { + size = target_size; + radius = size * 0.5f; + setGraphic(gGourceSettings.file_graphic); + } + } +} + +unsigned int RFile::getFileSize() const { + return file_size; +} + void RFile::remove(time_t removed_timestamp) { last_action = elapsed; fade_start = elapsed; @@ -175,28 +229,41 @@ float RFile::getAlpha() const{ void RFile::logic(float dt) { Pawn::logic(dt); - vec2 dest_pos = dest; + if(gGourceSettings.scale_by_file_size && size_transition_elapsed < size_transition_duration) { + size_transition_elapsed = std::min(size_transition_duration, size_transition_elapsed + dt); + float t = size_transition_elapsed / size_transition_duration; + // Smoothstep easing to avoid abrupt starts/stops. + float eased = t * t * (3.0f - 2.0f * t); + size = size_transition_start + (target_size - size_transition_start) * eased; + radius = size * 0.5f; + setGraphic(gGourceSettings.file_graphic); + } + + // Only apply spiral layout forces when NOT using physics-based file sizing + if (!gGourceSettings.scale_by_file_size) { + vec2 dest_pos = dest; /* - if(dir->getParent() != 0 && dir->noDirs()) { - vec2 dirnorm = dir->getNodeNormal(); - dest_pos = dirnorm + dest; - }*/ + if(dir->getParent() != 0 && dir->noDirs()) { + vec2 dirnorm = dir->getNodeNormal(); + dest_pos = dirnorm + dest; + }*/ - dest_pos = dest_pos * distance; + dest_pos = dest_pos * distance; - accel = dest_pos - pos; + accel = dest_pos - pos; - // apply accel - vec2 accel2 = accel * speed * dt; + // apply accel + vec2 accel2 = accel * speed * dt; - if(glm::length2(accel2) > glm::length2(accel)) { - accel2 = accel; - } + if(glm::length2(accel2) > glm::length2(accel)) { + accel2 = accel; + } - pos += accel2; + pos += accel2; - //files have no momentum - accel = vec2(0.0f, 0.0f); + //files have no momentum + accel = vec2(0.0f, 0.0f); + } if(fade_start < 0.0f && gGourceSettings.file_idle_time > 0.0f && (elapsed - last_action) > gGourceSettings.file_idle_time) { fade_start = elapsed; @@ -283,11 +350,13 @@ void RFile::drawNameText(float alpha) { float name_alpha = selected ? 1.0 : alpha; + std::string label = gGourceSettings.file_extensions ? ext : name; + if(selected) { - file_selected_font.draw(screenpos.x, screenpos.y, name); + file_selected_font.draw(screenpos.x, screenpos.y, label.c_str()); } else { file_font.setAlpha(name_alpha); - file_font.draw(screenpos.x, screenpos.y, gGourceSettings.file_extensions ? ext : name); + file_font.draw(screenpos.x, screenpos.y, label.c_str()); } } diff --git a/src/file.h b/src/file.h index e8b75e87..04a998cd 100644 --- a/src/file.h +++ b/src/file.h @@ -40,6 +40,11 @@ class RFile : public Pawn { float last_action; float radius; + unsigned int file_size; + float target_size; + float size_transition_start; + float size_transition_elapsed; + float size_transition_duration; vec2 dest; float distance; @@ -75,6 +80,8 @@ class RFile : public Pawn { void setDest(const vec2 & dest){ this->dest = dest; } void setDistance(float distance){ this->distance = distance; } + void setFileSize(unsigned int file_size); + unsigned int getFileSize() const; void calcScreenPos(GLint* viewport, GLdouble* modelview, GLdouble* projection); diff --git a/src/formats/commitlog.cpp b/src/formats/commitlog.cpp index 67f5b51b..9a6ef5e4 100644 --- a/src/formats/commitlog.cpp +++ b/src/formats/commitlog.cpp @@ -39,6 +39,7 @@ std::string RCommitLog::filter_utf8(const std::string& str) { RCommitLog::RCommitLog(const std::string& logfile, int firstChar) { + logfile_path = logfile; logf = 0; seekable = false; success = false; @@ -146,6 +147,10 @@ std::string RCommitLog::getLogCommand() { return log_command; } +const std::string& RCommitLog::getLogfilePath() const { + return logfile_path; +} + bool RCommitLog::isSeekable() { return seekable; } @@ -287,7 +292,7 @@ bool RCommitLog::createTempFile(std::string& temp_file) { // RCommitFile -RCommitFile::RCommitFile(const std::string& filename, const std::string& action, vec3 colour) { +RCommitFile::RCommitFile(const std::string& filename, const std::string& action, vec3 colour, unsigned int file_size) { this->filename = RCommitLog::filter_utf8(filename); @@ -298,6 +303,7 @@ RCommitFile::RCommitFile(const std::string& filename, const std::string& action, this->action = action; this->colour = colour; + this->file_size = file_size; } RCommit::RCommit() { @@ -318,11 +324,11 @@ vec3 RCommit::fileColour(const std::string& filename) { } } -void RCommit::addFile(const std::string& filename, const std::string& action) { - addFile(filename, action, fileColour(filename)); +void RCommit::addFile(const std::string& filename, const std::string& action, unsigned int file_size) { + addFile(filename, action, fileColour(filename), file_size); } -void RCommit::addFile(const std::string& filename, const std::string& action, const vec3& colour) { +void RCommit::addFile(const std::string& filename, const std::string& action, const vec3& colour, unsigned int file_size) { //check filename against filters if(!gGourceSettings.file_filters.empty()) { @@ -347,7 +353,7 @@ void RCommit::addFile(const std::string& filename, const std::string& action, c } } - files.push_back(RCommitFile(filename, action, colour)); + files.push_back(RCommitFile(filename, action, colour, file_size)); } void RCommit::postprocess() { diff --git a/src/formats/commitlog.h b/src/formats/commitlog.h index b95f0cd7..e6984801 100644 --- a/src/formats/commitlog.h +++ b/src/formats/commitlog.h @@ -35,8 +35,9 @@ class RCommitFile { std::string filename; std::string action; vec3 colour; + unsigned int file_size; - RCommitFile(const std::string& filename, const std::string& action, vec3 colour); + RCommitFile(const std::string& filename, const std::string& action, vec3 colour, unsigned int file_size); }; class RCommit { @@ -44,14 +45,15 @@ class RCommit { public: time_t timestamp; std::string username; + std::string commit_hash; std::list files; void postprocess(); bool isValid(); - void addFile(const std::string& filename, const std::string& action); - void addFile(const std::string& filename, const std::string& action, const vec3& colour); + void addFile(const std::string& filename, const std::string& action, unsigned int file_size = 0); + void addFile(const std::string& filename, const std::string& action, const vec3& colour, unsigned int file_size = 0); RCommit(); void debug(); @@ -64,6 +66,7 @@ class RCommitLog { std::string temp_file; std::string log_command; + std::string logfile_path; std::string lastline; @@ -93,6 +96,7 @@ class RCommitLog { bool checkFormat(); std::string getLogCommand(); + const std::string& getLogfilePath() const; static int systemCommand(const std::string& command); void requireExecutable(const std::string& exename); diff --git a/src/formats/custom.cpp b/src/formats/custom.cpp index f8335a4b..4baa0f7e 100644 --- a/src/formats/custom.cpp +++ b/src/formats/custom.cpp @@ -17,8 +17,7 @@ #include "custom.h" #include "../gource_settings.h" - -Regex custom_regex("^(?:\\xEF\\xBB\\xBF)?([^|]+)\\|([^|]*)\\|([ADM]?)\\|([^|]+)(?:\\|#?([a-fA-F0-9]{6}))?"); +#include CustomLog::CustomLog(const std::string& logfile) : RCommitLog(logfile) { } @@ -48,27 +47,25 @@ bool CustomLog::parseCommit(RCommit& commit) { bool CustomLog::parseCommitEntry(RCommit& commit) { std::string line; - std::vector entries; - if(!getNextLine(line)) return false; - //custom line - if(!custom_regex.match(line, &entries)) return false; + std::vector parts; + boost::split(parts, line, boost::is_any_of("|")); - time_t timestamp; + if (parts.size() < 4) return false; - // Allow timestamp to be a string - if(entries[0].size() > 1 && entries[0].find("-", 1) != std::string::npos) { - if(!SDLAppSettings::parseDateTime(entries[0], timestamp)) + time_t timestamp; + if(parts[0].size() > 1 && parts[0].find("-", 1) != std::string::npos) { + if(!SDLAppSettings::parseDateTime(parts[0], timestamp)) return false; } else { - timestamp = (time_t) atoll(entries[0].c_str()); - if(!timestamp && entries[0] != "0") + timestamp = (time_t) atoll(parts[0].c_str()); + if(!timestamp && parts[0] != "0") return false; } - std::string username = (entries[1].size()>0) ? entries[1] : "Unknown"; - std::string action = (entries[2].size()>0) ? entries[2] : "A"; + std::string username = (parts[1].size()>0) ? parts[1] : "Unknown"; + std::string action = (parts[2].size()>0) ? parts[2] : "A"; //if this file is for the same person and timestamp //we add to the commit, else we save the lastline @@ -85,16 +82,21 @@ bool CustomLog::parseCommitEntry(RCommit& commit) { bool has_colour = false; vec3 colour; - - if(entries.size()>=5 && entries[4].size()>0) { + + if (parts.size() >= 5 && parts[4].size() > 0) { has_colour = true; - colour = parseColour(entries[4]); + colour = parseColour(parts[4]); + } + + unsigned int file_size = 0; + if ((action == "A" || action == "M") && parts.size() >= 6 && parts[5].size() > 0) { + file_size = std::stoul(parts[5]); } if(has_colour) { - commit.addFile(entries[3], action, colour); + commit.addFile(parts[3], action, colour, file_size); } else { - commit.addFile(entries[3], action); + commit.addFile(parts[3], action, file_size); } return true; diff --git a/src/formats/git.cpp b/src/formats/git.cpp index ef3a482b..bad6cb1b 100644 --- a/src/formats/git.cpp +++ b/src/formats/git.cpp @@ -17,6 +17,12 @@ #include "git.h" #include "../gource_settings.h" +#include "../core/logger.h" +#include "../core/sdlapp.h" +#include +#include +#include +#include #ifndef _MSC_VER #include @@ -35,6 +41,51 @@ int git_version_patch = 0; Regex git_version_regex("([0-9]+)(?:\\.([0-9]+))?(?:\\.([0-9]+))?"); +namespace { + +bool parseRawGitLine(const std::string& line, + std::string& status, + std::string& filename, + std::string& dst_blob, + bool& has_blob_hash) { + if(line.empty() || line[0] != ':') return false; + + size_t tab = line.find('\t'); + if(tab == std::string::npos || tab == line.size()-1) return false; + + std::string header = line.substr(1, tab-1); + filename = line.substr(tab + 1); + if(filename.empty()) return false; + + std::istringstream header_stream(header); + std::vector fields; + std::string field; + while(header_stream >> field) fields.push_back(field); + + if(fields.empty()) return false; + + if(fields.size() >= 5) { + status = fields[4]; + } else { + status = fields.back(); + } + + if(status.empty()) return false; + status = status.substr(0,1); + + if(fields.size() >= 4) { + dst_blob = fields[3]; + has_blob_hash = true; + } else { + dst_blob.clear(); + has_blob_hash = false; + } + + return true; +} + +} + void GitCommitLog::readGitVersion() { if(git_version_major != 0) return; @@ -89,7 +140,7 @@ std::string GitCommitLog::logCommand() { std::string log_command = "git log " "--reverse --raw --encoding=UTF-8 " - "--no-renames"; + "--no-renames --abbrev=40"; readGitVersion(); @@ -104,9 +155,9 @@ std::string GitCommitLog::logCommand() { } if(gGourceSettings.author_time) { - log_command += " --pretty=format:user:%aN%n%at"; + log_command += " --pretty=format:user:%aN%n%at%n%H"; } else { - log_command += " --pretty=format:user:%aN%n%ct"; + log_command += " --pretty=format:user:%aN%n%ct%n%H"; } if(!gGourceSettings.start_date.empty()) { @@ -127,7 +178,12 @@ std::string GitCommitLog::logCommand() { return log_command; } -GitCommitLog::GitCommitLog(const std::string& logfile) : RCommitLog(logfile, 'u') { +GitCommitLog::GitCommitLog(const std::string& logfile) + : RCommitLog(logfile, 'u'), + m_repository_path(is_dir ? getLogfilePath() : std::string("")), + m_blob_sizes(), + m_blob_index_ready(false), + m_warned_missing_blob_size(false) { log_command = logCommand(); @@ -138,10 +194,94 @@ GitCommitLog::GitCommitLog(const std::string& logfile) : RCommitLog(logfile, 'u' if(logf) { success = true; seekable = true; + + if(gGourceSettings.scale_by_file_size) { + m_blob_index_ready = buildBlobSizeIndex(logfile); + if(!m_blob_index_ready) { + warnLog("failed to build git blob-size index for %s; file-size scaling may be unavailable", logfile.c_str()); + } + } } } } +bool GitCommitLog::buildBlobSizeIndex(const std::string& dir) { + m_blob_sizes.clear(); + + std::string temp_blob_file; + if(!createTempFile(temp_blob_file)) return false; + + char cwd_buff[1024]; + if(getcwd(cwd_buff, 1024) != cwd_buff) { + remove(temp_blob_file.c_str()); + return false; + } + + if(chdir(dir.c_str()) != 0) { + remove(temp_blob_file.c_str()); + return false; + } + + char cmd_buff[2048]; + int written = 0; +#ifdef _WIN32 + written = snprintf(cmd_buff, + sizeof(cmd_buff), + "git cat-file --batch-all-objects --batch-check=\"%%(objectname) %%(objecttype) %%(objectsize)\" > \"%s\"", + temp_blob_file.c_str()); +#else + written = snprintf(cmd_buff, + sizeof(cmd_buff), + "git cat-file --batch-all-objects --batch-check='%%(objectname) %%(objecttype) %%(objectsize)' > \"%s\"", + temp_blob_file.c_str()); +#endif + + int command_rc = -1; + if(written > 0 && written < (int)sizeof(cmd_buff)) { + command_rc = systemCommand(cmd_buff); + } + + int chdir_rc = chdir(cwd_buff); + if(chdir_rc != 0) { + remove(temp_blob_file.c_str()); + return false; + } + + if(command_rc != 0) { + remove(temp_blob_file.c_str()); + return false; + } + + std::ifstream in(temp_blob_file.c_str()); + if(!in.is_open()) { + remove(temp_blob_file.c_str()); + return false; + } + + std::string line; + while(std::getline(in, line)) { + std::istringstream ss(line); + std::string hash; + std::string type; + std::string size_str; + + if(!(ss >> hash >> type >> size_str)) continue; + if(type != "blob") continue; + + unsigned long long blob_size = strtoull(size_str.c_str(), 0, 10); + if(blob_size > std::numeric_limits::max()) { + blob_size = std::numeric_limits::max(); + } + + m_blob_sizes[hash] = (unsigned int)blob_size; + } + + in.close(); + remove(temp_blob_file.c_str()); + + return true; +} + BaseLog* GitCommitLog::generateLog(const std::string& dir) { //get working directory char cwd_buff[1024]; @@ -176,13 +316,18 @@ BaseLog* GitCommitLog::generateLog(const std::string& dir) { int written = snprintf(cmd_buff, 2048, "%s > %s", command.c_str(), temp_file.c_str()); if(written < 0 || written >= 2048) { + int restore_rc = chdir(cwd_buff); + (void)restore_rc; return 0; } int command_rc = systemCommand(cmd_buff); //change back to original directory - chdir(cwd_buff); + int chdir_rc = chdir(cwd_buff); + if(chdir_rc != 0) { + return 0; + } if(command_rc != 0) { return 0; @@ -215,30 +360,70 @@ bool GitCommitLog::parseCommit(RCommit& commit) { //this isnt a commit we are parsing, abort if(commit.timestamp == 0) return false; + if(!logf->getNextLine(line)) return false; + commit.commit_hash = line; + continue; } //should see username before files if(commit.username.empty()) return false; - size_t tab = line.find('\t'); - - //incorrect log format - if(tab == std::string::npos || tab == 0 || tab == line.size()-1) continue; - - std::string status = line.substr(tab - 1, 1); - std::string file = line.substr(tab + 1); - - if(file.empty()) continue; - - //check for and remove double quotes - if(file.find('"') == 0 && file.rfind('"') == file.size()-1) { - if(file.size()<=2) continue; - - file = file.substr(1,file.size()-2); + if (line[0] == ':') { + std::string status; + std::string filename; + std::string dst_blob; + bool has_blob_hash = false; + + if(!parseRawGitLine(line, status, filename, dst_blob, has_blob_hash)) { + if(gGourceSettings.scale_by_file_size) { + throw SDLAppException("git raw log line is missing blob hash metadata required for --scale-by-file-size"); + } + continue; + } + + unsigned int file_size = 0; + if(gGourceSettings.scale_by_file_size && status != "D") { + if(!has_blob_hash || dst_blob.empty() || dst_blob.find_first_not_of('0') == std::string::npos) { + throw SDLAppException("git raw log line is missing destination blob hash required for --scale-by-file-size"); + } + + if(!m_blob_index_ready) { + throw SDLAppException("--scale-by-file-size requires a Git repository path so blob sizes can be indexed"); + } + + std::unordered_map::const_iterator sit = m_blob_sizes.find(dst_blob); + if(sit != m_blob_sizes.end()) { + file_size = sit->second; + } else if(!m_warned_missing_blob_size) { + warnLog("missing blob size for object %s in %s, defaulting file size to 0", + dst_blob.c_str(), + m_repository_path.c_str()); + m_warned_missing_blob_size = true; + } + } + + commit.addFile(filename, status, file_size); + } else { + size_t tab = line.find('\t'); + + //incorrect log format + if(tab == std::string::npos || tab == 0 || tab == line.size()-1) continue; + + std::string status = line.substr(tab - 1, 1); + std::string file = line.substr(tab + 1); + + if(file.empty()) continue; + + //check for and remove double quotes + if(file.find('"') == 0 && file.rfind('"') == file.size()-1) { + if(file.size()<=2) continue; + + file = file.substr(1,file.size()-2); + } + + commit.addFile(file, status); } - - commit.addFile(file, status); } //check we at least got a username diff --git a/src/formats/git.h b/src/formats/git.h index cc5d1d90..a06665d6 100644 --- a/src/formats/git.h +++ b/src/formats/git.h @@ -19,8 +19,17 @@ #define GITLOG_H #include "commitlog.h" +#include class GitCommitLog : public RCommitLog { +private: + std::string m_repository_path; + std::unordered_map m_blob_sizes; + bool m_blob_index_ready; + bool m_warned_missing_blob_size; + + bool buildBlobSizeIndex(const std::string& dir); + protected: bool parseCommit(RCommit& commit); BaseLog* generateLog(const std::string& dir); diff --git a/src/gource.cpp b/src/gource.cpp index cf86c4f9..8ee5a40b 100644 --- a/src/gource.cpp +++ b/src/gource.cpp @@ -24,6 +24,31 @@ int gGourceMaxQuadTreeDepth = 6; int gGourceUserInnerLoops = 0; +static std::string formatFileSize(unsigned int bytes) { + static const char* units[] = {"B", "KB", "MB", "GB", "TB"}; + + if(bytes < 1024) { + return std::to_string(bytes) + " B"; + } + + double value = (double) bytes; + unsigned int unit_index = 0; + + while(value >= 1024.0 && unit_index < (sizeof(units) / sizeof(units[0])) - 1) { + value /= 1024.0; + unit_index++; + } + + char buf[64]; + if(value >= 100.0) { + snprintf(buf, sizeof(buf), "%.0f %s", value, units[unit_index]); + } else { + snprintf(buf, sizeof(buf), "%.1f %s", value, units[unit_index]); + } + + return std::string(buf); +} + Gource::Gource(FrameExporter* exporter) { this->logfile = gGourceSettings.path; @@ -1003,6 +1028,7 @@ RFile* Gource::addFile(const RCommitFile& cf) { int tagid = tag_seq++; RFile* file = new RFile(cf.filename, cf.colour, vec2(0.0,0.0), tagid); + file->setFileSize(cf.file_size); files[cf.filename] = file; @@ -1229,7 +1255,12 @@ void Gource::processCommit(const RCommit& commit, float t) { } std::map::iterator seen_file = files.find(cf.filename); - if(seen_file != files.end()) file = seen_file->second; + if(seen_file != files.end()) { + file = seen_file->second; + if (cf.action == "M") { + file->setFileSize(cf.file_size); + } + } if(file == 0) { file = addFile(cf); @@ -2371,6 +2402,7 @@ void Gource::drawFiles(float dt) { } else { root->drawFiles(dt); } + } void Gource::drawUsers(float dt) { @@ -2678,6 +2710,9 @@ void Gource::draw(float t, float dt) { textbox.setText(hoverFile->getName()); if(display_path.size()) textbox.addLine(display_path); + if (gGourceSettings.show_file_size_on_hover) { + textbox.addLine(formatFileSize(hoverFile->getFileSize())); + } textbox.setColour(hoverFile->getColour()); textbox.setPos(mousepos, true); diff --git a/src/gource_settings.cpp b/src/gource_settings.cpp index 76ec9479..014b60d2 100644 --- a/src/gource_settings.cpp +++ b/src/gource_settings.cpp @@ -80,7 +80,11 @@ void GourceSettings::help(bool extended_help) { printf(" --author-time Use the timestamp of the author instead of\n"); printf(" the timestamp of the committer\n"); printf(" -c, --time-scale SCALE Change simulation time scale (default: 1.0)\n"); - printf(" -e, --elasticity FLOAT Elasticity of nodes (default: 0.0)\n\n"); + printf(" -e, --elasticity FLOAT Elasticity of nodes (default: 0.0)\n"); + printf(" -S, --scale-by-file-size Scale file nodes by file size.\n"); + printf(" --file-size-scale-interpolation FACTOR\n"); + printf(" 0.0=uniform, 1.0=log, 2.0=linear (default: 1.0)\n\n"); + printf(" \n"); printf(" --key Show file extension key\n\n"); @@ -187,6 +191,14 @@ if(extended_help) { printf(" --hash-seed SEED Change the seed of hash function.\n\n"); + printf(" --file-scale FACTOR Scale factor for file nodes (default: 1.0).\n"); + printf(" --file-size-scale-interpolation FACTOR Blend file-size scaling curve\n"); + printf(" (0.0=uniform, 1.0=log, 2.0=linear)\n"); + printf(" --dir-spacing FACTOR Spacing for directory nodes (default: 1.0).\n"); + printf(" --file-gravity FACTOR Gravity for file nodes (default: 0.000001).\n"); + printf(" --file-repulsion FACTOR Repulsion for file nodes (default: 1000.0).\n"); + printf(" --show-file-size-on-hover Show file size on hover.\n\n"); + printf(" --path PATH\n\n"); } @@ -230,6 +242,7 @@ GourceSettings::GourceSettings() { arg_aliases["H"] = "extended-help"; arg_aliases["b"] = "background-colour"; arg_aliases["c"] = "time-scale"; + arg_aliases["S"] = "scale-by-file-size"; arg_aliases["background"] = "background-colour"; arg_aliases["disable-bloom"] = "hide-bloom"; arg_aliases["disable-progress"] = "hide-progress"; @@ -363,6 +376,14 @@ GourceSettings::GourceSettings() { arg_types["filename-time"] = "float"; arg_types["dir-name-depth"] = "int"; + + arg_types["scale-by-file-size"] = "bool"; + arg_types["file-size-scale-interpolation"] = "float"; + arg_types["file-scale"] = "float"; + arg_types["dir-spacing"] = "float"; + arg_types["file-gravity"] = "float"; + arg_types["file-repulsion"] = "float"; + arg_types["show-file-size-on-hover"] = "bool"; } void GourceSettings::setGourceDefaults() { @@ -472,6 +493,16 @@ void GourceSettings::setGourceDefaults() { user_friction = 1.0f; user_scale = 1.0f; + scale_by_file_size = false; + file_size_scale_interpolation = 1.0f; + file_scale = 1.0f; + + dir_spacing = 1.5f; + // Adjusted defaults for linear gravity and weak repulsion for tight packing + file_gravity = 0.002f; // Linear gravity (dist), constant pull toward center + file_repulsion = 1000.0f; // Inverse repulsion (strength/dist), weak for tight clusters + show_file_size_on_hover = false; + follow_users.clear(); highlight_users.clear(); highlight_all_users = false; @@ -1452,6 +1483,77 @@ void GourceSettings::importGourceSettings(ConfFile& conffile, ConfSection* gourc highlight_dirs = true; } + bool has_file_size_scale_interpolation = false; + + if(gource_settings->getBool("scale-by-file-size")) { + scale_by_file_size = true; + } + + if((entry = gource_settings->getEntry("file-size-scale-interpolation")) != 0) { + + if(!entry->hasValue()) conffile.entryException(entry, "specify file-size-scale-interpolation (float)"); + + file_size_scale_interpolation = entry->getFloat(); + + if(file_size_scale_interpolation < 0.0f || file_size_scale_interpolation > 2.0f) { + conffile.entryException(entry, "file-size-scale-interpolation outside of range 0.0 - 2.0 (inclusive)"); + } + + has_file_size_scale_interpolation = true; + } + + if(has_file_size_scale_interpolation) { + scale_by_file_size = file_size_scale_interpolation > 0.0f; + } + + if((entry = gource_settings->getEntry("file-scale")) != 0) { + + if(!entry->hasValue()) conffile.entryException(entry, "specify file-scale (float)"); + + file_scale = entry->getFloat(); + + if(file_scale <= 0.0f) { + conffile.invalidValueException(entry); + } + } + + if((entry = gource_settings->getEntry("dir-spacing")) != 0) { + + if(!entry->hasValue()) conffile.entryException(entry, "specify dir-spacing (float)"); + + dir_spacing = entry->getFloat(); + + if(dir_spacing <= 0.0f) { + conffile.invalidValueException(entry); + } + } + + if((entry = gource_settings->getEntry("file-gravity")) != 0) { + + if(!entry->hasValue()) conffile.entryException(entry, "specify file-gravity (float)"); + + file_gravity = entry->getFloat(); + + if(file_gravity <= 0.0f) { + conffile.invalidValueException(entry); + } + } + + if((entry = gource_settings->getEntry("file-repulsion")) != 0) { + + if(!entry->hasValue()) conffile.entryException(entry, "specify file-repulsion (float)"); + + file_repulsion = entry->getFloat(); + + if(file_repulsion <= 0.0f) { + conffile.invalidValueException(entry); + } + } + + if(gource_settings->getBool("show-file-size-on-hover")) { + show_file_size_on_hover = true; + } + if((entry = gource_settings->getEntry("camera-mode")) != 0) { if(!entry->hasValue()) conffile.entryException(entry, "specify camera-mode (overview,track)"); diff --git a/src/gource_settings.h b/src/gource_settings.h index 1975ec59..e0213c27 100644 --- a/src/gource_settings.h +++ b/src/gource_settings.h @@ -136,6 +136,14 @@ class GourceSettings : public SDLAppSettings { float user_scale; float time_scale; + bool scale_by_file_size; + float file_size_scale_interpolation; + float file_scale; + float dir_spacing; + float file_gravity; + float file_repulsion; + bool show_file_size_on_hover; + bool highlight_dirs; bool highlight_all_users; diff --git a/src/pawn.cpp b/src/pawn.cpp index 34646f73..8f3769b6 100644 --- a/src/pawn.cpp +++ b/src/pawn.cpp @@ -25,6 +25,8 @@ Pawn::Pawn(const std::string& name, vec2 pos, int tagid) { this->tagid = tagid; this->hidden = false; this->speed = 1.0; + this->vel = vec2(0.0f, 0.0f); + this->accel = vec2(0.0f, 0.0f); selected = false; mouseover = false; diff --git a/src/pawn.h b/src/pawn.h index 4c3389fb..a2467241 100644 --- a/src/pawn.h +++ b/src/pawn.h @@ -34,7 +34,9 @@ class Pawn : public QuadItem { std::string name; float namewidth; +public: vec2 accel; + vec2 vel; float speed; float elapsed;