Bladeren bron

major refactor to store metrics vice points

purpose of this was so if the point values do change they can be
appropriately reflected instead of applied after the fact
Bryan Allred 10 jaren geleden
bovenliggende
commit
76fd07cf50
7 gewijzigde bestanden met toevoegingen van 302 en 184 verwijderingen
  1. 136 110
      Dashboard.cs
  2. 0 18
      Penalties.cs
  3. 55 0
      Player.cs
  4. 109 0
      Points.cs
  5. 0 1
      PollingService.cs
  6. 0 53
      Rewards.cs
  7. 2 2
      pulse.csproj

+ 136 - 110
Dashboard.cs

96
		/// <value>
96
		/// <value>
97
		/// The points.
97
		/// The points.
98
		/// </value>
98
		/// </value>
99
		private IDictionary<string, double> Points { get; set; }
99
		private IDictionary<string, double> PointValues { get; set; }
100
100
101
		/// <summary>
101
		/// <summary>
102
		/// Gets or sets the projects.
102
		/// Gets or sets the projects.
112
		/// <value>
112
		/// <value>
113
		/// The scores.
113
		/// The scores.
114
		/// </value>
114
		/// </value>
115
		private IDictionary<string, double> Scores { get; set; }
115
		private IList<Player> Players { get; set; }
116
116
117
		/// <summary>
117
		/// <summary>
118
		/// Gets the width.
118
		/// Gets the width.
148
			this.DateRefresh = null;
148
			this.DateRefresh = null;
149
			this.Server = server;
149
			this.Server = server;
150
			this.SelectedItem = 1;
150
			this.SelectedItem = 1;
151
			this.Scores = new Dictionary<string, double>();
151
			this.Players = new List<Player>();
152
			this.HighlightedScores = new List<string>();
152
			this.HighlightedScores = new List<string>();
153
			this.Points = new Dictionary<string, double>()
154
			{
155
				{ Penalties.LackingVerbosity, -0.2 },
156
				{ Penalties.LackOfPurpose, -0.2 },
157
				{ Rewards.BugReport, 0.7 },
158
				{ Rewards.Collaboration, 0.7 },
159
				{ Rewards.CreatedWork, 0.02 },
160
				{ Rewards.DueDiligence, 0.4 },
161
				{ Rewards.FinishWork, 0.5 },
162
				{ Rewards.HouseCleaning, 0.1 },
163
				{ Rewards.Participation, 0.3 },
164
				{ Rewards.TakingOwnership, 0.1 },
165
				{ Rewards.Verbosity, 0.8 }
166
			};
153
			this.PointValues = Points.DefaultValues();
167
154
168
			var cacheFile = new FileInfo(this.CacheFile);
155
			var cacheFile = new FileInfo(this.CacheFile);
169
156
198
								break;
185
								break;
199
186
200
							case "score":
187
							case "score":
201
								var points = Convert.ToDouble(fields[2]);
188
								var pointMetrics = Points.EmptyDefaultValues();
189
								for (var i = 2; i < fields.Length; i++)
190
								{
191
									var parts = fields[i].Split(new char[] { '=' });
192
									if (parts.Length == 2)
193
									{
194
										double metricValue;
195
										if (!double.TryParse(parts[1], out metricValue))
196
										{
197
											metricValue = 0.0;
198
										}
199
200
										if (pointMetrics.ContainsKey(parts[0]))
201
										{
202
											pointMetrics[parts[0]] = metricValue;
203
										}
204
										else
205
										{
206
											pointMetrics.Add(parts[0], metricValue);
207
										}
208
									}
209
								}
202
210
203
								if (!this.Scores.ContainsKey(fields[1]))
211
								if (this.Players.Any(x => x.Name.Equals(fields[1], StringComparison.InvariantCultureIgnoreCase)))
204
								{
212
								{
205
									this.Scores.Add(fields[1], points);
213
									this.Players.First(x => x.Name.Equals(fields[1], StringComparison.InvariantCultureIgnoreCase)).PointMetrics = pointMetrics;
206
								}
214
								}
207
								else
215
								else
208
								{
216
								{
209
									this.Scores[fields[1]] = points;
217
									var player = new Player(fields[1]);
218
									player.PointMetrics = pointMetrics;
219
									this.Players.Add(player);
210
								}
220
								}
211
221
212
								break;
222
								break;
214
							case "point":
224
							case "point":
215
								var pointValue = Convert.ToDouble(fields[2]);
225
								var pointValue = Convert.ToDouble(fields[2]);
216
226
217
								if (!this.Points.ContainsKey(fields[1]))
227
								if (!this.PointValues.ContainsKey(fields[1]))
218
								{
228
								{
219
									this.Points.Add(fields[1], pointValue);
229
									this.PointValues.Add(fields[1], pointValue);
220
								}
230
								}
221
								else
231
								else
222
								{
232
								{
223
									this.Points[fields[1]] = pointValue;
233
									this.PointValues[fields[1]] = pointValue;
224
								}
234
								}
225
235
226
								break;
236
								break;
243
253
244
				var personOfInterest = commit.CommitterDisplayName;
254
				var personOfInterest = commit.CommitterDisplayName;
245
				this.AddHighlightedScore(personOfInterest);
255
				this.AddHighlightedScore(personOfInterest);
256
				var player = this.Players.FirstOrDefault(x => x.Name.Equals(personOfInterest, StringComparison.InvariantCultureIgnoreCase));
246
257
247
				if (!this.Scores.Any(x => x.Key.Equals(personOfInterest, StringComparison.InvariantCultureIgnoreCase)))
258
				if (player == null)
248
				{
259
				{
249
					this.Scores.Add(personOfInterest, 0);
260
					player = new Player(personOfInterest);
261
					this.Players.Add(player);
250
				}
262
				}
251
263
252
				// Get a point for participation.
264
				// Get a point for participation.
253
				this.Scores[personOfInterest] += this.GetPoints(Rewards.Participation);
265
				player.PointMetrics[Points.Participation]++;
254
266
255
				// Handle points for commits with comments.
267
				// Handle points for commits with comments.
256
				if (string.IsNullOrWhiteSpace(commit.Comment))
268
				if (string.IsNullOrWhiteSpace(commit.Comment))
257
				{
269
				{
258
					this.Scores[personOfInterest] += this.GetPoints(Penalties.LackingVerbosity);
270
					player.PointMetrics[Points.LackingVerbosity]++;
259
				}
271
				}
260
				else
272
				else
261
				{
273
				{
262
					this.Scores[personOfInterest] += this.GetPoints(Rewards.Verbosity);
274
					player.PointMetrics[Points.Verbosity]++;
263
				}
275
				}
264
276
265
				// Handle points with associating commits with actual work.
277
				// Handle points with associating commits with actual work.
266
				if (commit.AssociatedWorkItems.Count() == 0)
278
				if (commit.AssociatedWorkItems.Count() == 0)
267
				{
279
				{
268
					this.Scores[personOfInterest] += this.GetPoints(Penalties.LackOfPurpose);
280
					player.PointMetrics[Points.LackOfPurpose]++;
269
				}
281
				}
270
				else
282
				else
271
				{
283
				{
272
					this.Scores[personOfInterest] += this.GetPoints(Rewards.Collaboration);
284
					player.PointMetrics[Points.Collaboration]++;
273
				}
285
				}
274
			}
286
			}
275
		}
287
		}
294
				// Add to the overall tally.
306
				// Add to the overall tally.
295
				this.WorkItems++;
307
				this.WorkItems++;
296
308
297
				if (!string.IsNullOrWhiteSpace(item.CreatedBy))
298
				{
299
					if (!this.Scores.Any(x => x.Key.Equals(item.CreatedBy, StringComparison.InvariantCultureIgnoreCase)))
300
					{
301
						this.Scores.Add(item.CreatedBy, 0);
302
					}
303
				}
304
305
				if (item.CreatedBy != item.ChangedBy)
309
				if (item.CreatedBy != item.ChangedBy)
306
				{
310
				{
307
					if (!string.IsNullOrWhiteSpace(item.ChangedBy))
311
					if (!string.IsNullOrWhiteSpace(item.ChangedBy))
308
					{
312
					{
309
						if (!this.Scores.Any(x => x.Key.Equals(item.ChangedBy, StringComparison.InvariantCultureIgnoreCase)))
313
						var player = this.Players.FirstOrDefault(x => x.Name.Equals(item.ChangedBy, StringComparison.InvariantCultureIgnoreCase));
314
315
						if (player == null)
310
						{
316
						{
311
							this.Scores.Add(item.ChangedBy, 0);
317
							player = new Player(item.ChangedBy);
318
							this.Players.Add(player);
312
						}
319
						}
313
320
314
						// Add points for collaboration.
321
						// Add points for collaboration.
315
						this.Scores[item.ChangedBy] += this.GetPoints(Rewards.Collaboration);
322
						player.PointMetrics[Points.Collaboration]++;
316
					}
323
					}
317
				}
324
				}
318
				else
325
				else
319
				{
326
				{
320
					if (!string.IsNullOrWhiteSpace(item.CreatedBy))
327
					if (!string.IsNullOrWhiteSpace(item.CreatedBy))
321
					{
328
					{
322
						// They must have updated something right?
323
						this.Scores[item.CreatedBy] += this.GetPoints(Rewards.Participation);
329
						var player = this.Players.FirstOrDefault(x => x.Name.Equals(item.CreatedBy, StringComparison.InvariantCultureIgnoreCase));
330
331
						if (player == null)
332
						{
333
							player = new Player(item.CreatedBy);
334
							this.Players.Add(player);
335
						}
336
337
						// Add points for collaboration.
338
						player.PointMetrics[Points.Participation]++;
324
					}
339
					}
325
				}
340
				}
326
341
331
346
332
				if (!string.IsNullOrWhiteSpace(personOfInterest))
347
				if (!string.IsNullOrWhiteSpace(personOfInterest))
333
				{
348
				{
334
					switch (item.Type.Name.ToLower())
349
					var player = this.Players.FirstOrDefault(x => x.Name.Equals(personOfInterest, StringComparison.InvariantCultureIgnoreCase));
350
					if (player != null)
335
					{
351
					{
336
						case "product backlog item":
337
							this.Scores[personOfInterest] += this.GetPoints(Rewards.Participation);
338
							break;
339
340
						case "user story":
341
							this.Scores[personOfInterest] += this.GetPoints(Rewards.Participation);
342
							break;
343
344
						case "feature":
345
							this.Scores[personOfInterest] += this.GetPoints(Rewards.Participation);
346
							break;
347
348
						case "task":
349
							this.Scores[personOfInterest] += this.GetPoints(Rewards.CreatedWork);
350
							break;
351
352
						case "impediment":
353
						case "issue":
354
						case "bug":
355
							this.Scores[personOfInterest] += this.GetPoints(Rewards.BugReport);
356
							break;
357
358
						case "test case":
359
						case "shared steps":
360
							this.Scores[personOfInterest] += this.GetPoints(Rewards.CreatedWork);
361
							break;
362
363
						default:
364
							Debug.WriteLine("Did not handle work item type [" + item.Type.Name + "]");
365
							break;
366
					}
352
						switch (item.Type.Name.ToLower())
353
						{
354
							case "product backlog item":
355
								player.PointMetrics[Points.Participation]++;
356
								break;
367
357
368
					switch (item.State.ToLower())
369
					{
370
						case "new":
371
						case "design":
372
						case "to do":
373
						case "open":
374
						case "approved":
375
						case "active":
376
						case "in progress":
377
							this.Scores[personOfInterest] += this.GetPoints(Rewards.TakingOwnership);
378
							break;
379
380
						case "done":
381
						case "committed":
382
						case "resolved":
383
						case "closed":
384
							this.Scores[personOfInterest] += this.GetPoints(Rewards.FinishWork);
385
							break;
386
387
						case "removed":
388
							this.Scores[personOfInterest] += this.GetPoints(Rewards.HouseCleaning);
389
							break;
390
391
						default:
392
							Debug.WriteLine("Did not handle work item state [" + item.State + "]");
393
							break;
358
							case "user story":
359
								player.PointMetrics[Points.Participation]++;
360
								break;
361
362
							case "feature":
363
								player.PointMetrics[Points.Participation]++;
364
								break;
365
366
							case "task":
367
								player.PointMetrics[Points.CreatedWork]++;
368
								break;
369
370
							case "impediment":
371
							case "issue":
372
							case "bug":
373
								player.PointMetrics[Points.BugReport]++;
374
								break;
375
376
							case "test case":
377
							case "shared steps":
378
								player.PointMetrics[Points.CreatedWork]++;
379
								break;
380
381
							default:
382
								Debug.WriteLine("Did not handle work item type [" + item.Type.Name + "]");
383
								break;
384
						}
385
386
						switch (item.State.ToLower())
387
						{
388
							case "new":
389
							case "design":
390
							case "to do":
391
							case "open":
392
							case "approved":
393
							case "active":
394
							case "in progress":
395
								player.PointMetrics[Points.TakingOwnership]++;
396
								break;
397
398
							case "done":
399
							case "committed":
400
							case "resolved":
401
							case "closed":
402
								player.PointMetrics[Points.FinishWork]++;
403
								break;
404
405
							case "removed":
406
								player.PointMetrics[Points.HouseCleaning]++;
407
								break;
408
409
							default:
410
								Debug.WriteLine("Did not handle work item state [" + item.State + "]");
411
								break;
412
						}
394
					}
413
					}
395
				}
414
				}
396
			}
415
			}
410
				writer.WriteLine(string.Format("{0};{1}", "work", this.WorkItems));
429
				writer.WriteLine(string.Format("{0};{1}", "work", this.WorkItems));
411
				writer.WriteLine(string.Format("{0};{1}", "commits", this.Commits));
430
				writer.WriteLine(string.Format("{0};{1}", "commits", this.Commits));
412
431
413
				foreach (var score in this.Scores)
432
				foreach (var player in this.Players)
414
				{
433
				{
415
					writer.WriteLine(string.Format("{0};{1};{2}", "score", score.Key, score.Value));
434
					writer.WriteLine(string.Format(
435
						"{0};{1};{2}",
436
						"score",
437
						player.Name, string.Join(";", player.PointMetrics.Select(x => string.Format("{0}={1}", x.Key, x.Value)).ToArray())));
416
				}
438
				}
417
439
418
				foreach (var point in this.Points)
440
				foreach (var point in this.PointValues)
419
				{
441
				{
420
					writer.WriteLine(string.Format("{0};{1};{2}", "point", point.Key, point.Value));
442
					writer.WriteLine(string.Format("{0};{1};{2}", "point", point.Key, point.Value));
421
				}
443
				}
508
			{
530
			{
509
				Sound.Queue.Enqueue(Sound.Notify);
531
				Sound.Queue.Enqueue(Sound.Notify);
510
			}
532
			}
533
534
			// Clear the displayed highlighting.
535
			this.ClearHighlights();
511
		}
536
		}
512
537
513
		/// <summary>
538
		/// <summary>
516
		/// <param name="person">The person.</param>
541
		/// <param name="person">The person.</param>
517
		private void AddHighlightedScore(string person)
542
		private void AddHighlightedScore(string person)
518
		{
543
		{
519
			if (this.Scores.ContainsKey(person) && !this.HighlightedScores.Contains(person))
544
			if (this.Players.Any(x => x.Name.Equals(person, StringComparison.InvariantCultureIgnoreCase)) && !this.HighlightedScores.Contains(person))
520
			{
545
			{
521
				this.HighlightedScores.Add(person);
546
				this.HighlightedScores.Add(person);
522
			}
547
			}
529
		/// <returns>Returns the point value for a specific action.</returns>
554
		/// <returns>Returns the point value for a specific action.</returns>
530
		private double GetPoints(string action)
555
		private double GetPoints(string action)
531
		{
556
		{
532
			if (this.Points.ContainsKey(action))
557
			if (this.PointValues.ContainsKey(action))
533
			{
558
			{
534
				return this.Points[action];
559
				return this.PointValues[action];
535
			}
560
			}
536
561
537
			return 0.0;
562
			return 0.0;
567
		/// <returns></returns>
592
		/// <returns></returns>
568
		private IEnumerable<string> Leadboard()
593
		private IEnumerable<string> Leadboard()
569
		{
594
		{
570
			if (this.Scores == null || this.Scores.Count() == 0)
595
			if (this.Players == null || this.Players.Count() == 0)
571
			{
596
			{
572
				return new List<string>();
597
				return new List<string>();
573
			}
598
			}
574
599
575
			var rank = 0;
600
			var rank = 0;
576
			return this.Scores
601
			return this.Players
602
				.Select(x => new KeyValuePair<string, double>(x.Name, x.Score(this.PointValues)))
577
				.OrderByDescending(x => x.Value)
603
				.OrderByDescending(x => x.Value)
578
				.Select(x =>
604
				.Select(x =>
579
				{
605
				{

+ 0 - 18
Penalties.cs

1
namespace pulse
2
{
3
	/// <summary>
4
	/// Penalty point values.
5
	/// </summary>
6
	public static class Penalties
7
	{
8
		/// <summary>
9
		/// The lacking verbosity.
10
		/// </summary>
11
		public const string LackingVerbosity = "LackingVerbosity";
12
13
		/// <summary>
14
		/// The lack of purpose.
15
		/// </summary>
16
		public const string LackOfPurpose = "LackOfPurpose";
17
	}
18
}

+ 55 - 0
Player.cs

1
using System.Collections.Generic;
2
3
namespace pulse
4
{
5
	/// <summary>
6
	/// Player class.
7
	/// </summary>
8
	public class Player
9
	{
10
		/// <summary>
11
		/// Gets or sets the name.
12
		/// </summary>
13
		/// <value>
14
		/// The name.
15
		/// </value>
16
		public string Name { get; set; }
17
18
		/// <summary>
19
		/// Gets or sets the point metrics.
20
		/// </summary>
21
		/// <value>
22
		/// The point metrics.
23
		/// </value>
24
		public IDictionary<string, double> PointMetrics { get; set; }
25
26
		/// <summary>
27
		/// Initializes a new instance of the <see cref="Player"/> class.
28
		/// </summary>
29
		public Player(string name)
30
		{
31
			this.Name = name;
32
			this.PointMetrics = Points.EmptyDefaultValues();
33
		}
34
35
		/// <summary>
36
		/// Scores the specified point values.
37
		/// </summary>
38
		/// <param name="pointValues">The point values.</param>
39
		/// <returns>The score with the given values per metric.</returns>
40
		public double Score(IDictionary<string, double> pointValues)
41
		{
42
			var score = 0.0;
43
44
			foreach (var metric in this.PointMetrics)
45
			{
46
				if (pointValues.ContainsKey(metric.Key))
47
				{
48
					score += metric.Value * pointValues[metric.Key];
49
				}
50
			}
51
52
			return score;
53
		}
54
	}
55
}

+ 109 - 0
Points.cs

1
using System.Collections.Generic;
2
3
namespace pulse
4
{
5
	/// <summary>
6
	/// Point values.
7
	/// </summary>
8
	public static class Points
9
	{
10
		/// <summary>
11
		/// The bug report.
12
		/// </summary>
13
		public const string BugReport = "BugReport";
14
15
		/// <summary>
16
		/// The collaboration.
17
		/// </summary>
18
		public const string Collaboration = "Collaboration";
19
20
		/// <summary>
21
		/// The created work.
22
		/// </summary>
23
		public const string CreatedWork = "CreatedWork";
24
25
		/// <summary>
26
		/// The due diligence.
27
		/// </summary>
28
		public const string DueDiligence = "DueDiligence";
29
30
		/// <summary>
31
		/// The finish work.
32
		/// </summary>
33
		public const string FinishWork = "FinishWork";
34
35
		/// <summary>
36
		/// The house cleaning.
37
		/// </summary>
38
		public const string HouseCleaning = "HouseCleaning";
39
40
		/// <summary>
41
		/// The lacking verbosity.
42
		/// </summary>
43
		public const string LackingVerbosity = "LackingVerbosity";
44
45
		/// <summary>
46
		/// The lack of purpose.
47
		/// </summary>
48
		public const string LackOfPurpose = "LackOfPurpose";
49
50
		/// <summary>
51
		/// The participation.
52
		/// </summary>
53
		public const string Participation = "Participation";
54
55
		/// <summary>
56
		/// The taking ownership.
57
		/// </summary>
58
		public const string TakingOwnership = "TakingOwnership";
59
60
		/// <summary>
61
		/// The verbosity.
62
		/// </summary>
63
		public const string Verbosity = "Verbosity";
64
65
		/// <summary>
66
		/// Defaults the values.
67
		/// </summary>
68
		/// <returns>Default values for points.</returns>
69
		public static IDictionary<string, double> DefaultValues()
70
		{
71
			return new Dictionary<string, double>()
72
			{
73
				{ Points.LackingVerbosity, -0.2 },
74
				{ Points.LackOfPurpose, -0.2 },
75
				{ Points.BugReport, 0.7 },
76
				{ Points.Collaboration, 0.7 },
77
				{ Points.CreatedWork, 0.02 },
78
				{ Points.DueDiligence, 0.4 },
79
				{ Points.FinishWork, 0.5 },
80
				{ Points.HouseCleaning, 0.1 },
81
				{ Points.Participation, 0.3 },
82
				{ Points.TakingOwnership, 0.1 },
83
				{ Points.Verbosity, 0.8 }
84
			};
85
		}
86
87
		/// <summary>
88
		/// Defaults the values zeroed.
89
		/// </summary>
90
		/// <returns></returns>
91
		public static IDictionary<string, double> EmptyDefaultValues()
92
		{
93
			return new Dictionary<string, double>()
94
			{
95
				{ Points.LackingVerbosity, 0 },
96
				{ Points.LackOfPurpose, 0 },
97
				{ Points.BugReport, 0 },
98
				{ Points.Collaboration, 0 },
99
				{ Points.CreatedWork, 0 },
100
				{ Points.DueDiligence, 0 },
101
				{ Points.FinishWork, 0 },
102
				{ Points.HouseCleaning, 0 },
103
				{ Points.Participation, 0 },
104
				{ Points.TakingOwnership, 0 },
105
				{ Points.Verbosity, 0 }
106
			};
107
		}
108
	}
109
}

+ 0 - 1
PollingService.cs

50
			base.OnDoWork(e);
50
			base.OnDoWork(e);
51
51
52
			var now = DateTime.Now;
52
			var now = DateTime.Now;
53
			this.Dashboard.ClearHighlights();
54
			Debug.WriteLine("Worker started at " + DateTime.Now);
53
			Debug.WriteLine("Worker started at " + DateTime.Now);
55
54
56
			if (e.Cancel)
55
			if (e.Cancel)

+ 0 - 53
Rewards.cs

1
namespace pulse
2
{
3
	/// <summary>
4
	/// Reward point values.
5
	/// </summary>
6
	public static class Rewards
7
	{
8
		/// <summary>
9
		/// The bug report.
10
		/// </summary>
11
		public const string BugReport = "BugReport";
12
13
		/// <summary>
14
		/// The collaboration.
15
		/// </summary>
16
		public const string Collaboration = "Collaboration";
17
18
		/// <summary>
19
		/// The created work.
20
		/// </summary>
21
		public const string CreatedWork = "CreatedWork";
22
23
		/// <summary>
24
		/// The due diligence.
25
		/// </summary>
26
		public const string DueDiligence = "DueDiligence";
27
28
		/// <summary>
29
		/// The finish work.
30
		/// </summary>
31
		public const string FinishWork = "FinishWork";
32
33
		/// <summary>
34
		/// The house cleaning.
35
		/// </summary>
36
		public const string HouseCleaning = "HouseCleaning";
37
38
		/// <summary>
39
		/// The participation.
40
		/// </summary>
41
		public const string Participation = "Participation";
42
43
		/// <summary>
44
		/// The taking ownership.
45
		/// </summary>
46
		public const string TakingOwnership = "TakingOwnership";
47
48
		/// <summary>
49
		/// The verbosity.
50
		/// </summary>
51
		public const string Verbosity = "Verbosity";
52
	}
53
}

+ 2 - 2
pulse.csproj

42
  </ItemGroup>
42
  </ItemGroup>
43
  <ItemGroup>
43
  <ItemGroup>
44
    <Compile Include="Dashboard.cs" />
44
    <Compile Include="Dashboard.cs" />
45
    <Compile Include="Penalties.cs" />
45
    <Compile Include="Player.cs" />
46
    <Compile Include="PollingService.cs">
46
    <Compile Include="PollingService.cs">
47
      <SubType>Component</SubType>
47
      <SubType>Component</SubType>
48
    </Compile>
48
    </Compile>
49
    <Compile Include="Program.cs" />
49
    <Compile Include="Program.cs" />
50
    <Compile Include="Properties\AssemblyInfo.cs" />
50
    <Compile Include="Properties\AssemblyInfo.cs" />
51
    <Compile Include="Rewards.cs" />
51
    <Compile Include="Points.cs" />
52
    <Compile Include="Sound.cs" />
52
    <Compile Include="Sound.cs" />
53
    <Compile Include="TfsCredentialsProvider.cs" />
53
    <Compile Include="TfsCredentialsProvider.cs" />
54
  </ItemGroup>
54
  </ItemGroup>