A pleasant walk through computing

Comment for me? Send an email. I might even update the post!

Notes on Improving Developer Team Practices

I've been wondering lately about what practices, if changed or improved, will do the most to help a software development team.

Personally, I'm interested in maintainability and automated systems. My question is: how to design so it's as easy as I can make it for someone else to bring my code up-to-date. This is never really just swapping out old stuff. I've been convinced for years that such a nirvana is not only unattainable, but wrong to seek. But having a museum of programming approaches makes technical debt hard to pay. It's not a matter of changing technology, or embracing yesterday's shiny article. It's building into the environment the practice of maintaining old code as a requirement. The adage "if it ain't broke don't fix it" doesn't apply to a software-dependent company (if it applies anywhere). The Japanese word kaizen indicates a better approach: continuous change for the better.

Here are some notes I've made. By doing this, and based on years of experience and study, I came to a conclusion that won't be astounding to most people, but bears calling out.

Improving team communication will yield the biggest long-term benefit in the shortest time.

Key Goals

  • Maintain
  • Automate
  • Test
  • Communicate

Key Principles and Practices

  • Prefer smaller changes rolled out frequently
  • Reduce platforms and frameworks
  • Have routine, effective code reviews to improve maintainability and communication
  • Ruthlessly improve code to be "as if had always been that way"
  • Struggle with naming. To name well requires understanding how others think
  • Have routine strategy sessions away from the workplace
  • Use in-house templates and libraries for consistency
  • Maintain all code as if it were a top-tier open-source project
  • Use smaller projects to try new technologies. Either the new project will get normalized, or pieces from it will be applied to existing projects. (Or some combination of the two.)

Key Areas

  • Personnel Interaction/Culture
  • Security
  • Project Management
  • Continuous Integration and Deployment
  • Source Control
  • Testing
  • Web UI framework
  • Web Api framework
  • ORM
  • Database
  • Architecture (DDD, etc.)

There will be natural overlap in these areas.

Improving which of these yields the most return? I would look at what improves communication first (project mgmt, Agile-based methods, studies on successful teams). Without communication and agreement, it's very difficult to improve other areas. I also think Security needs to be part of everyday work and thinking. Next, CI/CD, Testing, Source Control. Getting CI working will drive a lot of improvements. Then, the stack.

I'll either update this post or follow up on it in a second part.

Using Git Locally With Remote TFVC

I have a client who uses Microsoft Team Foundation Server on site, and has been using TFVC version control for about fifteen years. They have a very large codebase with lots of discrete and interconnected Visual Studio solutions.

I've recommended moving to Git, and they're cautiously interested. It would be a large, challenging and time-consuming transition, so for the time being, "interested" means "no."

I couldn't imagine giving up Git. I wondered how hard it would be to use Git locally, but still check in needed commits to their TFS server.

There are a couple of weird things, but using Git and TFVC together is pretty easy.

Important

I've only tested this in Visual Studio 2017, and if you're using Git you shouldn't be using any earlier VS version. You could, as long as you're willing to forego Git Visual Studio integration. Even 2015's integration doesn't work right.

Add to Source Control

This part's simple. You add the solution to the respective source controls as you normally would. With that said, I strongly recommend adding a .tfignore file, as well as a .gitignore.

You don't normally need a .tfignore because Visual Studio takes care of ignoring lots of files. But having one lets you ignore the Git folders.

The .tfignore file can be as simple as this:

.git
.git*

I use one of the standard Visual Studio .gitignore files, then add this:

.tfignore

The standard .gitignore will ignore the $tf folder.

Of course, you can elect to commit the ignore files. For instance, I sometimes keep both ignore files in the Git repository.

Checking In to TFVC

The problem with checking in is that Visual Studio has a setting for Source Control Plugin, and naturally can't figure out whether to use Git or TFVC. The default behavior seems to be that it prefers Git to TFVC.

If you're a Git user, your typical flow with the addition of TFVC will be:

  1. Make a bunch of code changes in a branch, committing to your local Git repo several times.
  2. When ready to integrate your changes to master, squash the commits, switch to master, and merge.
  3. Close the solution.
  4. Open Visual Studio, but not the solution.
  5. Open Team Explorer
  6. Connect to your TFS team project
  7. I like to right-click on the solution and choose Open in Source Code Explorer. I think this isolates the solution's changes from any other changes in the team project.
  8. Open Pending Changes
  9. Be sure to scroll to the bottom and open Exclude Changes.
  10. Promote the changes as needed. If your .tfignore file is right, these will only be new and project deleted files.
  11. Comment and check in. I use the same comment that I used for the squashed Git commits.

Reopening the Solution

You'll usually get the prompt below no matter how you open the solution, and therefore have to open the solution twice. I like opening it right after check in.

  1. Open the solution, either from the Start Page, or File > Open > Recent Projects and Solutions. You'll get this message because your Source Control Plugin was changed to Visual Studio Team Services, and VS recognizes that Git source control is available in your solution.
  2. Answer Yes. The solution will close. Reopen the solution and it should open without error.

Final Thoughts

This has been a huge help to me. I'm able to code effectively using Git's power, but still support my client's environment.

This setup is also a nice way to start showing the value of Git. For example, there's likely to be very little objection to also creating a Git repo on the TFS server, and using that to synchronize long-running branches. It protects the client by ensuring my latest code is available to them, but doesn't force me to use TFVC's ponderous branching and merging.

Raw Code - Picture Renaming and Sorting

A while back I needed to rename a bunch of pictures, prefixing them with numbers, so that they'd sort the way I wanted.

Here's the code I used. I ran this in LINQPad, but you could use it in a console app with minor modifications. One nice thing I learned to do: get image datetime without loading the whole image.

Hope this is useful to you in some way!

void Main()
{
	string path = @"C:\Users\charles\Pictures\Inbox\20160630";
	List<FileChange> list = new List<UserQuery.FileChange>();

    foreach (string fileName in Directory.GetFiles(path))
	{
		DateTime taken;
		try
		{
			taken = GetDateTakenFromImage(fileName);
		}
		catch
		{
			try
			{
				taken = GetDateTakenFromFilename(fileName);
			}
			catch
			{
				taken = File.GetLastWriteTime(fileName);
				if (File.GetCreationTime(fileName) < taken) { taken = File.GetCreationTime(fileName); }
			}
		}
		FileChange item = new FileChange(fileName, "", taken);
		//set new taken on to old.
		item.NewTakenOn = item.OldTakenOn;
		list.Add(item);
	}

	//sort and set new name
	int newNbr = 1;
	foreach (var item in list.OrderBy(l => l.OldTakenOn))
	{
		item.DestinationFileName = GetNewFileName(item.SourceFileName, newNbr);
		newNbr++;
	}
	
	DumpList(list.OrderBy(l => l.OldTakenOn).ToList());
	DumpList(list.OrderBy(l => l.NewTakenOn).ToList());
	
	//rename
	list.ForEach(a => File.Move(a.SourceFileName, a.DestinationFileName));
}

public static void DumpList(List<FileChange> list)
{
	var newList = list
	.Select(l => new { oldName = Path.GetFileName(l.SourceFileName), newName = Path.GetFileName(l.DestinationFileName), l.OldTakenOn, l.NewTakenOn });
	newList.Dump();
}

//http://stackoverflow.com/questions/180030/how-can-i-find-out-when-a-picture-was-actually-taken-in-c-sharp-running-on-vista
//we init this once so that if the function is repeatedly called
//it isn't stressing the garbage man
private static Regex r = new Regex(":");

//retrieves the datetime WITHOUT loading the whole image
public static DateTime GetDateTakenFromImage(string path)
{
	using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
	using (Image myImage = Image.FromStream(fs, false, false))
	{
		//if (!myImage.PropertyIdList.Any(a => a == 36867)){ return DateTime.Parse("1/1/1900");}
		PropertyItem propItem = myImage.GetPropertyItem(36867);
		string dateTaken = r.Replace(Encoding.UTF8.GetString(propItem.Value), "-", 2);
		//string t = Encoding.UTF8.GetString(propItem.Value);
		return DateTime.Parse(dateTaken);
	}
}

public static DateTime GetDateTakenFromFilename(string path)
{
	var takenPartSplit = path.Substring(3, 15).Split('_');
	string datePart = takenPartSplit[0];
	string timePart = takenPartSplit[1];
	datePart = datePart.Substring(0, 4) + "-" + datePart.Substring(4, 2) + "-" + datePart.Substring(6, 2);
	timePart = timePart.Substring(0, 2) + ":" + timePart.Substring(2, 2) + ":" + timePart.Substring(4, 2);
	string datetime = datePart + " " + timePart;
	return DateTime.Parse(datetime);
}

public static string GetNewFileName(string path, int newNumber)
{
	string oldName = Path.GetFileName(path);
	string newName = oldName;
	newName = newNumber.ToString().PadLeft(3, '0') + " " + newName;
	return path.Replace(oldName, newName);
}

public class FileChange
{
	public string SourceFileName { get; set; }
	public string DestinationFileName { get; set; }
	public DateTime OldTakenOn { get; set; }
	public DateTime NewTakenOn { get; set; }

	public FileChange(string source, string destination, DateTime takenOn)
	{
		SourceFileName = source;
		DestinationFileName = destination;
		OldTakenOn = takenOn;
	}
}