Tracking which version of your app you are working with can be difficult, especially when it leaves development (source code) and moves on to testers or end-users via the App Store (binary only). You may need to revisit older versions of your app for regression testing or bug stomping.
Many workflows use Git tags to simplify tracking code revisions.
Before submitting to the App Store you add the tag submission/1.1
for version
1.1 of your app. Once the submission is approved and released, the tag is
renamed to app-store/1.1
.
This can be simplified by instead (or additionally) adding the current git revision to your builds with a simple shell script and some swift.
This guide uses Swift, but Objective-C should work just as easily
First, we need to retrieve the hash for the current commit, which can be done on the command line:
git rev-parse HEAD
Now we need to integrate it into our app. What I’ve done is created a bash script and a code template and added them to my build process in Xcode.
Recently I've been creating a strings file on iOS. Similar to, but
different from, the
Android Strings.xml
files. In my case the file is pure
Swift. For those unfamiliar with Android development, this file is
where I would put all the user-facing strings, most of which need
translations later.
Set Up Structure
I create a directory called Localization
to store my strings files.
The directory structure looks like this:
Localization
├── Strings.swift
├── generated
│ └── Strings+Git.swift
└── src
├── Strings+Git.swift.tpl
└── make-git-strings.sh
2 directories, 4 files
The template file (.tpl
) and build script go in the src
subdirectory, while
the output goes to the generated
subdirectory. I never add the generated
swift file, but I do add the empty generated
directory to
git.
Create Code Template
My template file is an extension of an enum I already
have called kStrings
. Create the base enum Strings.swift
:
import Foundation
enum kStrings {
}
Create the git extension as a template, Strings+Git.swift.tpl
// GIT: $GIT_HASH
// vim: ft=swift
import Foundation
/**
* Git information extension for kStrings,
* Auto-generated by `Strings+Git.swift.tpl`.
* Do NOT Manually edit this extension
*/
extension kStrings {
enum Git {
enum Hash {
static let full = "$GIT_HASH"
static let short = full.substring(to: toIndex)
private static let toIndex = full.index(full.startIndex, offsetBy: 7)
}
}
}
Create Bash Script
Then the make-git-strings.sh
script can be as simple as:
GIT_HASH=$(git rev-parse HEAD)
FULL_PATH=$(dirname $0)
IN_FILE="$FULL_PATH/Strings+Git.swift.tpl"
OUT_FILE="$FULL_PATH/../generated/Strings+Git.swift"
rm -f $OUT_FILE
sed s/\$GIT_HASH/$GIT_HASH/g $IN_FILE > $OUT_FILE
We must run this script once before we are able to add our Strings+Git.swift
.
chmod u+x make-git-strings.sh
./make-git-strings.sh
Make sure to add the generated Strings+Git.swift
file to your .gitignore
before adding it
to your project, or you may accidentally commit the file to git. This
file should not be committed since we want to make sure it is generated
and updated with the right information each time. Instead, commit the
template file and make any changes there.
Add Generated Files to Project
Add the generated Strings+Git.swift
to your project.
Next, we add this shell script to our Xcode build process. This will make sure our build always has the latest git information in the code.
Go to your Project, then Build Phases and click the + to add a new Run Script Phase. In the script editor, enter the path to the bash script, in my case it looks like this:
${SRCROOT}/GitDemoApp/src/Localization/src/make-git-strings.sh
Change the path relative to $SRCROOT
to find the right spot.
The order here is also important. Make sure this step appears before the
Compile Sources step, since it’s generating one of those sources we want
to compile.
Usage Example
Use kStrings.Git.Hash.full
to get the full SHA-1 hash, or
kStrings.Git.Hash.short
to get a truncated (7-character) version of the hash.
Here’s an example view controller that shows both the full hash and short hash of the current git commit in a label:
import UIKit
class AboutViewController: UIViewController {
// MARK: - View Properties {{{
private lazy var aboutLabel: UILabel! = {
let label = UILabel(frame: .zero)
label.text = "Git Hash\n\(kStrings.Git.Hash.full)\n\n"
+ "Short Hash\n\(kStrings.Git.Hash.short)"
label.numberOfLines = 0
label.textAlignment = .center
label.sizeToFit()
return label
}()
// }}} View Properties
// MARK: - Life Cycle Methods {{{
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(aboutLabel)
initConstraints()
}
private func initConstraints() {
aboutLabel.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
aboutLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
aboutLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
]
NSLayoutConstraint.activate(constraints)
}
// }}} Life Cycle Methods
}
Improving the Script
To reduce any source of error when building a production (release) build to make sure I have all the latest changes committed, I enhance the script to check for a dirty working tree, and return a non-zero value to stop the build from continuing.
My builds have an extra Custom Build Setting called OBF_ENVIRONMENT
which tells me which environment the app is intended for. If the OBF_ENVIRONMENT
is PRODUCTION
and the source tree is dirty, I cancel the build.
There is also an option to force it anyway by adding --force
to the end
of the command line in case we need to create a release version for testing
bugs so we don’t have to commit changes constantly.
My enhance script looks like this:
GIT_HASH=$(git rev-parse HEAD)
FULL_PATH=$(dirname $0)
IN_FILE="$FULL_PATH/Strings+Git.swift.tpl"
OUT_FILE="$FULL_PATH/../generated/Strings+Git.swift"
TRUE=0
FALSE=1
function isDirty() {
changeOutput=$(git diff --shortstat 2> /dev/null | tail -n1)
if [[ -n "$changeOutput" ]]; then
# if output not null, it's dirty
echo "Git Dirty!"
return $TRUE
else
echo "Git Clean!"
return $FALSE
fi
}
if [[ $1 == "--force" ]]; then
echo "Should Force!"
shouldForce=$TRUE
else
echo "No Force!"
shouldForce=$FALSE
fi
if [[ $OBF_ENVIRONMENT == "PRODUCTION" ]] && isDirty \
&& [[ $shouldForce == $FALSE ]]; then
echo "Error: Attempted to produce production build with dirty working tree."
echo "Could not generate git hash strings file extension."
echo "Please commit your working copy and try again."
exit 1
fi
rm -f $OUT_FILE
sed s/\$GIT_HASH/$GIT_HASH/g $IN_FILE > $OUT_FILE
Conclusion
This technique is mostly for developer use. Apple still requires the version numbers of your app and build numbers to increment each time you submit to the App Store. So we still can’t replace a version number with the git hash completely. The git hash really shines just for figuring out exactly which git commit of the code you are looking at so you can check it out again.