diff --git a/src/Models/StatisticSample.cs b/src/Models/StatisticSample.cs
new file mode 100644
index 00000000..1e59371b
--- /dev/null
+++ b/src/Models/StatisticSample.cs
@@ -0,0 +1,15 @@
+namespace SourceGit.Models {
+ ///
+ /// 统计图表样品
+ ///
+ public class StatisticSample {
+ ///
+ /// 样品名
+ ///
+ public string Name { get; set; }
+ ///
+ /// 提交个数
+ ///
+ public int Count { get; set; }
+ }
+}
diff --git a/src/Resources/Icons.xaml b/src/Resources/Icons.xaml
index 9d50a679..47faa94b 100644
--- a/src/Resources/Icons.xaml
+++ b/src/Resources/Icons.xaml
@@ -35,6 +35,7 @@
M716.3 383.1c0 38.4-6.5 76-19.4 111.8l-10.7 29.7 229.6 229.5c44.5 44.6 44.5 117.1 0 161.6a113.6 113.6 0 01-80.8 33.5a113.6 113.6 0 01-80.8-33.5L529 694l-32 13a331.6 331.6 0 01-111.9 19.4A333.5 333.5 0 0150 383.1c0-39 6.8-77.2 20-113.6L285 482l194-195-214-210A331 331 0 01383.1 50A333.5 333.5 0 01716.3 383.1zM231.6 31.6l-22.9 9.9a22.2 22.2 0 00-5.9 4.2a19.5 19.5 0 000 27.5l215 215.2L288.4 417.8 77.8 207.1a26 26 0 00-17.2-7.1a22.8 22.8 0 00-21.5 15a400.5 400.5 0 00-7.5 16.6A381.6 381.6 0 000 384c0 211.7 172.2 384 384 384c44.3 0 87.6-7.5 129-22.3L743.1 975.8A163.5 163.5 0 00859.5 1024c43.9 0 85.3-17.1 116.4-48.2a164.8 164.8 0 000-233L745.5 513C760.5 471.5 768 428 768 384C768 172 596 0 384 0c-53 0-104 10.5-152.5 31.5z
M928 500a21 21 0 00-19-20L858 472a11 11 0 01-9-9c-1-6-2-13-3-19a11 11 0 015-12l46-25a21 21 0 0010-26l-8-22a21 21 0 00-24-13l-51 10a11 11 0 01-12-6c-3-6-6-11-10-17a11 11 0 011-13l34-39a21 21 0 001-28l-15-18a20 20 0 00-27-4l-45 27a11 11 0 01-13-1c-5-4-10-9-15-12a11 11 0 01-3-12l19-49a21 21 0 00-9-26l-20-12a21 21 0 00-27 6L650 193a9 9 0 01-11 3c-1-1-12-5-20-7a11 11 0 01-7-10l1-52a21 21 0 00-17-22l-23-4a21 21 0 00-24 14L532 164a11 11 0 01-11 7h-20a11 11 0 01-11-7l-17-49a21 21 0 00-24-15l-23 4a21 21 0 00-17 22l1 52a11 11 0 01-8 11c-5 2-15 6-19 7c-4 1-8 0-12-4l-33-40A21 21 0 00313 146l-20 12A21 21 0 00285 184l19 49a11 11 0 01-3 12c-5 4-10 8-15 12a11 11 0 01-13 1L228 231a21 21 0 00-27 4L186 253a21 21 0 001 28L221 320a11 11 0 011 13c-3 5-7 11-10 17a11 11 0 01-12 6l-51-10a21 21 0 00-24 13l-8 22a21 21 0 0010 26l46 25a11 11 0 015 12l0 3c-1 6-2 11-3 16a11 11 0 01-9 9l-51 8A21 21 0 0096 500v23A21 21 0 00114 544l51 8a11 11 0 019 9c1 6 2 13 3 19a11 11 0 01-5 12l-46 25a21 21 0 00-10 26l8 22a21 21 0 0024 13l51-10a11 11 0 0112 6c3 6 6 11 10 17a11 11 0 01-1 13l-34 39a21 21 0 00-1 28l15 18a20 20 0 0027 4l45-27a11 11 0 0113 1c5 4 10 9 15 12a11 11 0 013 12l-19 49a21 21 0 009 26l20 12a21 21 0 0027-6L374 832c3-3 7-5 10-4c7 3 12 5 20 7a11 11 0 018 10l-1 52a21 21 0 0017 22l23 4a21 21 0 0024-14l17-50a11 11 0 0111-7h20a11 11 0 0111 7l17 49a21 21 0 0020 15a19 19 0 004 0l23-4a21 21 0 0017-22l-1-52a11 11 0 018-10c8-3 13-5 18-7l1 0c6-2 9 0 11 3l34 41A21 21 0 00710 878l20-12a21 21 0 009-26l-18-49a11 11 0 013-12c5-4 10-8 15-12a11 11 0 0113-1l45 27a21 21 0 0027-4l15-18a21 21 0 00-1-28l-34-39a11 11 0 01-1-13c3-5 7-11 10-17a11 11 0 0112-6l51 10a21 21 0 0024-13l8-22a21 21 0 00-10-26l-46-25a11 11 0 01-5-12l0-3c1-6 2-11 3-16a11 11 0 019-9l51-8a21 21 0 0018-21v-23zm-565 188a32 32 0 01-51 5a270 270 0 011-363a32 32 0 0151 6l91 161a32 32 0 010 31zM512 782a270 270 0 01-57-6a32 32 0 01-20-47l92-160a32 32 0 0127-16h184a32 32 0 0130 41c-35 109-137 188-257 188zm15-328L436 294a32 32 0 0121-47a268 268 0 0155-6c120 0 222 79 257 188a32 32 0 01-30 41h-184a32 32 0 01-28-16z
+ M296 912H120c-4.4 0-8-3.6-8-8V520c0-4.4 3.6-8 8-8h176c4.4 0 8 3.6 8 8v384c0 4.4-3.6 8-8 8zM600 912H424c-4.4 0-8-3.6-8-8V121c0-4.4 3.6-8 8-8h176c4.4 0 8 3.6 8 8v783c0 4.4-3.6 8-8 8zM904 912H728c-4.4 0-8-3.6-8-8V280c0-4.4 3.6-8 8-8h176c4.4 0 8 3.6 8 8v624c0 4.4-3.6 8-8 8z
M550 627h-81v-21a142 142 0 0113-64a198 198 0 0152-57a390 390 0 0047-42a56 56 0 0012-34a58 58 0 00-21-45a81 81 0 00-56-19a85 85 0 00-57 20a103 103 0 00-32 59l-82-10a136 136 0 0149-96A172 172 0 01512 276a178 178 0 01123 40a122 122 0 0145 94a103 103 0 01-17 56a366 366 0 01-71 72A136 136 0 00556 576a128 128 0 00-6 51zm-81 120v-89h89v89zM512 64a448 448 0 10448 448A448 448 0 00512 64zm0 832a384 384 0 010-768a389 389 0 01384 384a389 389 0 01-384 384z
M64 864h896V288h-396a64 64 0 01-57-35L460 160H64v704zm-64 32V128a32 32 0 0132-32h448a32 32 0 0129 18L564 224H992a32 32 0 0132 32v640a32 32 0 01-32 32H32a32 32 0 01-32-32z
M448 64l128 128h448v768H0V64z
diff --git a/src/Resources/Locales/en_US.xaml b/src/Resources/Locales/en_US.xaml
index e6580b6c..53f73427 100644
--- a/src/Resources/Locales/en_US.xaml
+++ b/src/Resources/Locales/en_US.xaml
@@ -121,6 +121,7 @@
Open In Git Bash
Refresh
Search Commit
+ Statistics
Configure this repository
WORKSPACE
LOCAL BRANCHES
@@ -487,6 +488,14 @@
To :
Reword :
+ Statistics
+ WEEK
+ MONTH
+ Total Committers: {0}
+ Total Commits:{0}
+ COMMITTER
+ COMMITS
+
Git has NOT been configured. Please to go [Preference] and configure it first.
Path[{0}] not exists!
Can NOT locate bash.exe. Make sure bash.exe exists under the same folder with git.exe
diff --git a/src/Resources/Locales/zh_CN.xaml b/src/Resources/Locales/zh_CN.xaml
index ad67a4b2..7aefa601 100644
--- a/src/Resources/Locales/zh_CN.xaml
+++ b/src/Resources/Locales/zh_CN.xaml
@@ -120,6 +120,7 @@
在GIT终端中打开
重新加载
查找提交
+ 提交统计
配置本仓库
工作区
本地分支
@@ -486,6 +487,14 @@
合并到 :
修改提交信息:
+ 提交统计
+ 本周
+ 本月
+ 提交者人数:{0}
+ 总计提交次数:{0}
+ 提交者
+ 提交次数
+
GIT尚未配置。请打开【偏好设置】配置GIT路径。
路径({0})不存在或不可读取!
无法找到bash.exe,请确保其在git.exe同目录中!
diff --git a/src/Resources/Styles/TabControl.xaml b/src/Resources/Styles/TabControl.xaml
index 87afce73..69cc26a8 100644
--- a/src/Resources/Styles/TabControl.xaml
+++ b/src/Resources/Styles/TabControl.xaml
@@ -80,4 +80,37 @@
+
+
\ No newline at end of file
diff --git a/src/Views/Controls/Chart.cs b/src/Views/Controls/Chart.cs
new file mode 100644
index 00000000..faa0652d
--- /dev/null
+++ b/src/Views/Controls/Chart.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Media;
+
+namespace SourceGit.Views.Controls {
+ ///
+ /// 绘制提交频率柱状图
+ ///
+ public class Chart : FrameworkElement {
+ public static readonly int LABEL_UNIT = 32;
+ public static readonly double MAX_SHAPE_WIDTH = 24;
+
+ public static readonly DependencyProperty LineBrushProperty = DependencyProperty.Register(
+ "LineBrush",
+ typeof(Brush),
+ typeof(Chart),
+ new PropertyMetadata(Brushes.White));
+
+ public Brush LineBrush {
+ get { return (Brush)GetValue(LineBrushProperty); }
+ set { SetValue(LineBrushProperty, value); }
+ }
+
+ public static readonly DependencyProperty ChartBrushProperty = DependencyProperty.Register(
+ "ChartBrush",
+ typeof(Brush),
+ typeof(Chart),
+ new PropertyMetadata(Brushes.White));
+
+ public Brush ChartBrush {
+ get { return (Brush)GetValue(ChartBrushProperty); }
+ set { SetValue(ChartBrushProperty, value); }
+ }
+
+ private int maxV = 0;
+ private List samples = new List();
+
+ ///
+ /// 设置绘制数据
+ ///
+ /// 数据源
+ public void SetData(List samples) {
+ this.samples = samples;
+ maxV = 0;
+
+ foreach (var s in samples) {
+ if (maxV < s.Count) maxV = s.Count;
+ }
+
+ maxV = (int)Math.Ceiling(maxV / 10.0) * 10;
+ InvalidateVisual();
+ }
+
+ protected override void OnRender(DrawingContext dc) {
+ base.OnRender(dc);
+
+ var font = new FontFamily("Consolas");
+ var pen = new Pen(LineBrush, 1);
+ dc.DrawLine(pen, new Point(LABEL_UNIT, 0), new Point(LABEL_UNIT, ActualHeight - LABEL_UNIT));
+ dc.DrawLine(pen, new Point(LABEL_UNIT, ActualHeight - LABEL_UNIT), new Point(ActualWidth, ActualHeight - LABEL_UNIT));
+
+ if (samples.Count == 0) return;
+
+ var stepV = (ActualHeight - LABEL_UNIT) / 5;
+ var labelStepV = maxV / 5;
+ var gridPen = new Pen(LineBrush, 1) { DashStyle = DashStyles.Dash };
+ for (int i = 1; i < 5; i++) {
+ var vLabel = new FormattedText(
+ $"{maxV - i * labelStepV}",
+ CultureInfo.CurrentCulture,
+ FlowDirection.LeftToRight,
+ new Typeface(font, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
+ 12.0,
+ LineBrush,
+ VisualTreeHelper.GetDpi(this).PixelsPerDip);
+
+ var dashHeight = i * stepV;
+ var vy = Math.Max(0, dashHeight - vLabel.Height * 0.5);
+ dc.DrawLine(gridPen, new Point(LABEL_UNIT + 1, dashHeight), new Point(ActualWidth, dashHeight));
+ dc.DrawText(vLabel, new Point(0, vy));
+ }
+
+ var stepX = (ActualWidth - LABEL_UNIT) / samples.Count;
+ var shapeWidth = Math.Min(LABEL_UNIT, stepX - 4);
+ for (int i = 0; i < samples.Count; i++) {
+ var h = samples[i].Count * (ActualHeight - LABEL_UNIT) / maxV;
+ var x = LABEL_UNIT + 1 + stepX * i + (stepX - shapeWidth) * 0.5;
+ var y = ActualHeight - LABEL_UNIT - h;
+ var rect = new Rect(x, y, shapeWidth, h);
+
+ var hLabel = new FormattedText(
+ samples[i].Name,
+ CultureInfo.CurrentCulture,
+ FlowDirection.LeftToRight,
+ new Typeface(font, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
+ 10.0,
+ LineBrush,
+ VisualTreeHelper.GetDpi(this).PixelsPerDip);
+ var xLabel = x - (hLabel.Width - shapeWidth) * 0.5;
+ var yLabel = ActualHeight - LABEL_UNIT + 4;
+
+ dc.DrawRectangle(ChartBrush, null, rect);
+
+ if (stepX < LABEL_UNIT) {
+ dc.PushTransform(new TranslateTransform(xLabel, yLabel));
+ dc.PushTransform(new RotateTransform(45, hLabel.Width * 0.5, hLabel.Height * 0.5));
+ dc.DrawText(hLabel, new Point(0, 0));
+ dc.Pop();
+ dc.Pop();
+ } else {
+ dc.DrawText(hLabel, new Point(xLabel, yLabel));
+ }
+ }
+ }
+ }
+}
diff --git a/src/Views/Statistics.xaml b/src/Views/Statistics.xaml
new file mode 100644
index 00000000..68ab4b15
--- /dev/null
+++ b/src/Views/Statistics.xaml
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Views/Statistics.xaml.cs b/src/Views/Statistics.xaml.cs
new file mode 100644
index 00000000..25bf0062
--- /dev/null
+++ b/src/Views/Statistics.xaml.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace SourceGit.Views {
+ ///
+ /// 提交统计
+ ///
+ public partial class Statistics : Controls.Window {
+ private static readonly string[] WEEK_DAYS = new string[] { "一", "二", "三", "四", "五", "六", "日" };
+ private string repo = null;
+
+ public Statistics(string repo) {
+ this.repo = repo;
+ InitializeComponent();
+ Task.Run(Refresh);
+ }
+
+ private void Quit(object sender, RoutedEventArgs e) {
+ Close();
+ }
+
+ private void Refresh() {
+ var mapsWeek = new Dictionary();
+ for (int i = 0; i < 7; i++) {
+ mapsWeek.Add(i, new Models.StatisticSample {
+ Name = $"星期{WEEK_DAYS[i]}",
+ Count = 0,
+ });
+ }
+
+ var mapsMonth = new Dictionary();
+ var today = DateTime.Now;
+ var maxDays = DateTime.DaysInMonth(today.Year, today.Month);
+ for (int i = 1; i <= maxDays; i++) {
+ mapsMonth.Add(i, new Models.StatisticSample {
+ Name = $"{i}",
+ Count = 0,
+ });
+ }
+
+ var mapCommitterWeek = new Dictionary();
+ var mapCommitterMonth = new Dictionary();
+ var week = today.DayOfWeek;
+ var month = today.Month;
+
+ var limits = $"--since=\"{today.ToString("yyyy-MM-01 00:00:00")}\"";
+ var commits = new Commands.Commits(repo, limits).Result();
+ var totalCommitsMonth = commits.Count;
+ var totalCommitsWeek = 0;
+ foreach (var c in commits) {
+ var commitTime = DateTime.Parse(c.Committer.Time);
+ if (IsSameWeek(today, commitTime)) {
+ mapsWeek[(int)commitTime.DayOfWeek].Count++;
+ if (mapCommitterWeek.ContainsKey(c.Committer.Name)) {
+ mapCommitterWeek[c.Committer.Name].Count++;
+ } else {
+ mapCommitterWeek[c.Committer.Name] = new Models.StatisticSample {
+ Name = c.Committer.Name,
+ Count = 1,
+ };
+ }
+
+ totalCommitsWeek++;
+ }
+
+ mapsMonth[commitTime.Day].Count++;
+
+ if (mapCommitterMonth.ContainsKey(c.Committer.Name)) {
+ mapCommitterMonth[c.Committer.Name].Count++;
+ } else {
+ mapCommitterMonth[c.Committer.Name] = new Models.StatisticSample {
+ Name = c.Committer.Name,
+ Count = 1,
+ };
+ }
+ }
+
+ var samplesChartWeek = new List();
+ var samplesChartMonth = new List();
+ var samplesCommittersWeek = new List();
+ var samplesCommittersMonth = new List();
+ for (int i = 0; i < 7; i++) samplesChartWeek.Add(mapsWeek[i]);
+ for (int i = 1; i <= maxDays; i++) samplesChartMonth.Add(mapsMonth[i]);
+ foreach (var kv in mapCommitterWeek) samplesCommittersWeek.Add(kv.Value);
+ foreach (var kv in mapCommitterMonth) samplesCommittersMonth.Add(kv.Value);
+ mapsMonth.Clear();
+ mapsWeek.Clear();
+ mapCommitterMonth.Clear();
+ mapCommitterWeek.Clear();
+ commits.Clear();
+ samplesCommittersWeek.Sort((x, y) => y.Count - x.Count);
+ samplesCommittersMonth.Sort((x, y) => y.Count - x.Count);
+
+ Dispatcher.Invoke(() => {
+ loading.IsAnimating = false;
+ loading.Visibility = Visibility.Collapsed;
+
+ chartWeek.SetData(samplesChartWeek);
+ chartMonth.SetData(samplesChartMonth);
+
+ lstCommitterWeek.ItemsSource = samplesCommittersWeek;
+ lstCommitterMonth.ItemsSource = samplesCommittersMonth;
+
+ txtMemberCountWeek.Text = App.Text("Statistics.TotalCommitterCount", samplesCommittersWeek.Count);
+ txtMemberCountMonth.Text = App.Text("Statistics.TotalCommitterCount", samplesCommittersMonth.Count);
+ txtCommitCountWeek.Text = App.Text("Statistics.TotalCommitsCount", totalCommitsWeek);
+ txtCommitCountMonth.Text = App.Text("Statistics.TotalCommitsCount", totalCommitsMonth);
+ });
+ }
+
+ private bool IsSameWeek(DateTime t1, DateTime t2) {
+ double diffDay = t1.Subtract(t2).Duration().TotalDays;
+ if (diffDay >= 7) return false;
+
+ return t1.CompareTo(t2) > 0 ? (t1.DayOfWeek >= t2.DayOfWeek) : t1.DayOfWeek <= t2.DayOfWeek;
+ }
+ }
+}
diff --git a/src/Views/Widgets/Dashboard.xaml b/src/Views/Widgets/Dashboard.xaml
index da779d87..85984014 100644
--- a/src/Views/Widgets/Dashboard.xaml
+++ b/src/Views/Widgets/Dashboard.xaml
@@ -108,6 +108,13 @@
IsChecked="{Binding Source={x:Static models:Preference.Instance}, Path=Window.MoveCommitInfoRight, Mode=TwoWay, Converter={StaticResource InverseBool}}"
Checked="ChangeOrientation" Unchecked="ChangeOrientation"/>
+
+