Dealing with Xcode’s Git Limitations

Having explored Xcode’s Git support, it’s important to acknowledge that it doesn’t seem to be a popular choice among developers. I’ve repeatedly asked my 3,000 or so Twitter followers if anyone uses only Xcode for their Git needs, and to date, no one has said yes.

That presents two obvious questions: what does Xcode not do well enough (or at all), and what do people use instead?

Looking at the Git Index of Commands,[13] there are about 50 Git commands, some of which clearly don’t exist in Xcode, and probably wouldn’t make sense considering its UI. For example, you can’t bisect, which is a targeted series of checkouts meant to find which version introduced a behavior change. You can’t stash changes away while you switch branches. You can’t cherry-pick or rebase, which are powerful but dangerous ways of rewriting your commit history. For hard-core Git users, Xcode leaves quite a bit to be desired.

Many developers use either the git command line, or Mac GUI applications for managing Git repositories, like GitHub Desktop[14] (which only works with GitHub-hosted repositories), Tower,[15] SourceTree,[16] and others. Some developers use these apps exclusively and ignore Xcode features, while others use Xcode’s version comparison tools or even perform pushes, pulls, and basic branch management from within Xcode. It’s your choice of how much you do or do not let Xcode involve itself in source control

In this section, you’ll see a few real-world techniques for using Git with Xcode projects but without Xcode itself.

The .gitignore and .gitconfig Files

Xcode project files are actually bundles—directories that contain a file hierarchy of metadata for both the project itself and your use of it. This is how Xcode remembers things like what tabs you had opened the last time you worked in a project. Thing is, if we have many developers working on a project, none of them need to know about your tab settings or other preferences, so putting these files under source control is just noise to anyone looking at the Git history and a (small) waste of space.

To deal with this, you can create a file named .gitignore and place it at the top level of the repository (i.e., as a peer to the xcodeproj bundle). This file contains filenames and patterns with wildcard specifiers (*), and any matching files are versioned by Git. A starter .gitignore for an Xcode project can be as simple as the following:

 .DS_Store
 xcuserdata/

.DS_Store is the macOS path for local metadata, like file tags. All your user-specific settings are tracked by files in xcuserdata, so excluding that directory keeps those files out of Git. These are the big two you need for Xcode 9. You can also get language-specific .gitignore files for Objective-C and Swift from GitHub’s own gitignore project at https://github.com/github/gitignore. The names are actually a little misleading: rather than being language-specific, most of the files they screen out are bookkeeping files and build results used by older versions of Xcode, plus files created by third-party tools like CocoaPods, Carthage, and Fastlane.

Dot Files

images/aside-icons/tip.png

Keep in mind that since .gitignore starts with a period character, it’s invisible by default. Text editors like BBEdit and TextMate will warn you before saving a file that starts with a period, while the Finder won’t even allow you to rename a file to one that starts with a period. Of course, there’s always Terminal…

You can also create a file named .gitconfig to set Git properties to be used by all Git actions, whether via Xcode, the git command line, or something else. A .gitconfig in your home directory is used as a default, but you can override it with .gitconfig files in project directories. Most of the settings apply to the command line git, but one common use is to set a universal default name and email:

 [user]
  name = Prags iOS Test
  email = [email protected]

There are lots of things you can set in .gitconfig; if you’re going to use the command line, GitHub’s own “gitconfig” project has a sample file to get started: https://github.com/gitconfig/gitconfig.

Merging from the Command Line

If you use one of the apps mentioned earlier, they’ll provide their own UI for resolving merge conflicts, using a UI similar to Xcode’s. However, if you use the command line, the default behavior is to just leave you with a modified file that includes markers to show both sides of the conflict:

 let​ galahad = ​QuestionsThree​(name: ​"Galahad"​,
  quest: ​"Seek the Grail"​,
 <<<<<<< ​HEAD
  favoriteColor: .blue)
 =======
  favoriteColor: .yellow)
 >>>>>>> issue0005

The idea is that you’re supposed to manually edit these files with a text editor to resolve the conflict. You know, like a savage.

On macOS, what you want to do is to edit your .gitconfig to set your merge tool to opendiff, like this:

 [diff]
  tool = opendiff
 [merge]
  tool = opendiff

With this setting, once you perform a merge that creates conflicted files, you can use the command git mergetool to open the built-in file merge app, /Applications/Utilities/FileMerge. As shown in the figure, it uses a side-by-side display to compare the versions and evaluate the conflicts, and just like Xcode, you choose right, left, or both to resolve them. When used from the command line, you close the window and quit each time you finish resolving one file, and if there are more files in conflict, FileMerge launches again. And it has to be said: this is surely the ugliest GUI of any app Apple ships today. Still, better than editing those >>>>>>>> lines, right?

images/scm/filemerge-resolve-merge-conflict.png

Merging Project and Storyboard Files

Two Xcode file types are particularly susceptible to Git merge conflicts: project files and storyboards. They’re both text files, so Git thinks it can merge incompatible changes, and sometimes it can. But when you have to manually resolve a conflict, it’s both ugly and risky.

Merging Project Files

The easier of the two is project files. Project files aren’t magic; look at your project file with the Version Editor instead of the Standard Editor and you’ll see it kind of looks like a property list, with curly braces as containers, semicolons and commas as separators, and C-style /* ... */ comments. It’s at least as human-readable as JSON, maybe more so.

When you get a merge conflict, it’s usually a fairly simple matter of two branches adding new files to the same group, and they happen to get sorted into the same place in the file. But what makes it scary is that when you work from the git command line, the conflict markers basically corrupt the xcodeproj file and leave it unable to open, because its contents now look like this (note that line breaks have been added to accommodate the book’s formatting):

 <<<<<<< HEAD
  5E3567872017D77000DDE41F /* ThirdViewController.swift in Sources */ =
  {isa = PBXBuildFile; fileRef = 5E3567862017D77000DDE41F
  /* ThirdViewController.swift */; };
 =======
  01748CC42017D73D00720E55 /* SecondViewController.swift in Sources */ =
  {isa = PBXBuildFile; fileRef = 01748CC32017D73D00720E55
  /* SecondViewController.swift */; };
 >>>>>>> issue0007

This is actually an easy problem to fix: both developers added new files, they happen to have landed in the same place in the file, so you just want to keep both of them. Do a git mergetool and accept both sides of this conflict.

images/scm/filemerge-resolve-xcodeproj-merge-conflict.png

The other option for dealing with a project file merge conflict is to simply accept all of one side’s changes and ignore the others. What will likely happen is that the project won’t build, because references to the new files won’t be found—but that’s an obvious and easy to fix error. In this conflict, if you just took issue0007’s changes, then you’d lose ThirdViewController.swift from the project file, but then any code that calls ThirdViewController would fail to compile, and you’d know to just re-add that file in the File Navigator. Build again, commit, push, done.

Merging Storyboard Files

Storyboard merge problems are one of the most hated aspects of iOS and Mac development, and some developers justify not using storyboards entirely because of bad experiences with corrupted storyboard files.

First, you can largely eliminate this problem with a preventative measure: use storyboard references, as discussed in Storyboard References. You can’t have merge conflicts when different branches are modifying completely different files. This is a step many people overlook as their storyboards naturally grow along with the project, but you should really stop every once in a while and refactor large storyboards into smaller files by using references. There’s no right number of scenes to have in a storyboard, just enough so that you’re never tempted to have two developers in the same file at the same time. Or, if you do find you need to make concurrent edits like that, split the storyboard up into smaller files first.

Second, some storyboard conflicts are trivial. Storyboards are frequently marked as modified just because a user opened one. To see why, right-click or control-click a storyboard file to open it as “Source Code” instead of the usual “Interface Builder - Storyboard”. Notice that the storyboard is just a big XML file:

 <?xml version="1.0" encoding="UTF-8"?>
 <document type=​"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB"
  version=​"3.0"​ toolsVersion=​"13122.16"​ systemVersion=​"17A277"
  targetRuntime=​"iOS.CocoaTouch"​ propertyAccessControl=​"none"
  useAutolayout=​"YES"​ useTraitCollections=​"YES"​ useSafeAreas=​"YES"
  colorMatched=​"YES"​ initialViewController=​"BYZ-38-t0r"​>

Since the systemVersion and toolsVersion (i.e,. the version of Xcode) are baked into the file, any differences between macOS and Xcode versions among the project’s developers, including betas, appear as edits to the storyboard file. The good news is, this is a simple string substitution that doesn’t corrupt storyboard files. At worst, it just looks like a big revert war in the Git log until everyone standardizes their versions. Although, sure, it’s still really annoying.

Other elements of the XML are prone to simple conflicts. <color> elements sometimes change based on the color space provided by a developer’s computer. Older versions of Xcode used to have <rect> conflicts when the numeric values for coordinates or sizes would be integers or floating point based on whether the last person to touch the file had a Retina display. Seriously, you’d have revert wars where a value might change between 30 and 30.5 because one developer had a MacBook Pro and another had a MacBook Air. Fortunately, Xcode 9 seems to use integers everywhere, so this has actually improved.

Still, it’s possible for a merge conflict to fundamentally corrupt the XML structure, and make the storyboard file not open. There are still several ways to recover from this:

  • Do your best to make sense of the XML structure and edit it manually. Personally, I’ve successfully done this once or twice and it’s not fun, but if you’re looking at the two sides in FileMerge, you can sometimes understand what Git was thinking and set it right. Take ten minutes and give it a shot.

  • Forget about making the merge work, and just re-perform the easier set of storyboard changes. To do this, revert to the branch that made the most changes, then check out a second copy of the project to a separate location, and open the other branch that led to the conflict. Open the storyboard in that project, select and copy any new scenes and segues, then paste them into the first project. This will look like new changes on the first branch, and you’ll want to test them carefully.

It could be argued that major conflicts in project files and especially in storyboard files aren’t source control problems at all but rather engineering and project management problems: your storyboards are too big, you’re staying on branches too long, or your issues are too big and need to be broken down into multiple smaller issues. If you don’t want to have an ugly storyboard merge, adopt development practices that make them unlikely to occur in the first place, right?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.14.85.76