The “Violent Scrum Master”

Just heard about a video for an upcoming book called The Power of Scrum on Software Engineering Radio. It was pretty amusing, but I think gets the message across too.

Reverting changes in Git

Coming from Subversion, I’ve definitely got some pre-conceived notions about how reverting changes should work. For those that don’t know, svn revert is used to discard local changes in your working tree. It’s a command that I use often. Either because I find my change heading down the wrong path, or I simply was marking up the code while studying it’s functionality and I want to discard my changes.

When I first started using Git, reverting local changes was a big challenge. git revert is meant to revert a changeset, and not local changes, which did not match my mental model. I’ve been using Git everyday for a while now, and I’m still not happy with the situation. There are just a few too many edges in there, and it requires just a little too much thinking to pick out the right tool and take care of the job.

git checkout

The first time I saw this, I was confused. I kept asking myself “what does checking out have to do with discarding changes?” When you think about what git checkout does, it starts making a little more sense. From the git checkout manpage, we have:

Updates files in the working tree to match the version in the index or the specified tree. If no paths are given, git checkout will also update HEAD to set the specified branch as the current branch.

The key here is “update files in the working tree to match the version in the index.” Let me demonstrate. Let’s say you had a file called foo.txt, and we echoed some data to the end of it:

jszakmeister$ echo foo >> foo.txt

git status now shows us this:

jszakmeister$ git status -s
 M foo.txt

To revert the change that we just made, we’d run:

jszakmeister$ git checkout -- foo.txt

Checking the status, we see:

jszakmeister$ git status -s

So the change has been discarded. That’s because the version of the file in the index matches HEAD. We know that because we don’t have any staged changes. So the rule here is:

git checkout — will discard unstaged changes in <path>.

Keep in mind, you can discard changes for your entire working copy with:

jszakmeister$ git checkout -- .

Or,

jszakmeister$ git checkout HEAD

The former method has the nice effect of only discarding changes below your current working directory, if you’re further down in the tree… something that I did with Subversion quite often. Unlike Subversion, the command is recursive by default. Despite the risk of blowing away a tree’s worth of changes, I prefer git’s choice of being recursive by default.

git reset

Okay, so now we know how to discard unstaged changes, but what about staged changes? That’s where git reset comes into play. Unfortunately, it’s more complicated than I care for. The man page for git reset has this to say:

SYNOPSIS

git reset [-q] [] [—]
git reset —patch [] [—] […]
git reset [—soft | —mixed | —hard | —merge | —keep] [-q] []

DESCRIPTION

In the first and second form, copy entries from to the index. In the third form, set the current branch head (HEAD) to , optionally modifying index and working tree to match. The defaults to HEAD in all forms.

The take away here is that git reset changes the index to match a commit. And depending on the form, it may update your working tree as well.

Let’s go back to the previous example, except this time I’m going to add the change to the index:

jszakmeister$ echo foo >> foo.txt
jszakmeister$ git add foo.txt
jszakmeister$ git status -s
M  foo.txt

So you have a few choices at this point. Let’s walk through them.

Blow it all away

If you’re looking to discard all the changes, and get a clean working tree, simply run:

jszakmeister$ git reset --hard HEAD

This would be the “third form” mentioned above1. This resets foo.txt in the index to match HEAD, and also updates the working tree to match. At this point, we have a nice clean working tree:

jszakmeister$ git status -s

It’s a bit hard-core though. Chances are, if you have staged changes, you actually want to keep some of them. That’s when things get more interesting.

Selectively reverting staged changes

Let’s tweak our example just a little bit. Let’s say we have changes to foo.txt and bar.txt staged, and we only want to revert the changes to foo.txt. Here’s the status of our working tree:

jszakmeister$ git status -s
M  foo.txt
M  bar.txt

We don’t want to run git reset --hard HEAD at this point, as it would discard our changes to bar.txt. So, we’re left with a two step process. First, we unstage the change with a different form of git reset, and then we discard the unstaged change with git checkout:

jszakmeister$ git reset foo.txt
Unstaged changes after reset:
M   foo.txt
jszakmeister$ git checkout foo.txt
jszakmeister$ git status -s
M  bar.txt

We can see that our changes too foo.txt have been discarded. It just seems like a little too much work to get there though.

But wait! That’s not all!

I ran across this situation a few times now. I’m getting ready to make an initial commit, and I’ve added a file that I didn’t intend. My gut reaction is to run git reset to unstage the change. However, I’m met promptly with an error:

jszakmeister$ git status -s
A  foo.txt
jszakmesiter$ git reset foo.txt
fatal: Failed to resolve 'HEAD' as a valid ref.

The issue is that on a brand new repository, there is no HEAD. So the symbolic ref fails to resolve, and the command fails to execute. It feels like Git should set up HEAD to point to some default empty tree commit, but it doesn’t. So what do you do in this case? You remove it from the index:

jszakmeister$ git rm --cached foo.txt
rm 'foo.txt'
jszakmeister$ git status -s
?? foo.txt

3-to-1

So there you have it. Unfortunately, it takes three commands in Git to replicate what I could do with one command in Subversion. Am I ready to change back? No, the benefits of Git are just too great. Do I feel that there is still more room for Git to improve? You betcha. The nice part is that the community is genuinely concerned about making Git more friendly, and smoothing out the burrs. I hope this is one area that can be improved upon. Until things change, hopefully the above can serve as a reference for the various ways of reverting changes.


  1. Anyone else find it amusing to read “hard head” in that command line?  

The 99%…

A friend of mine from school posted a link on Facebook that I thought I should share with others. It’s to a site called We are the 99%. It contains many striking stories of those who have been impacted by the economy.

We are the 99%

It’s some really powerful stuff. I’ve been fortunate. My parents and my brother have not. They’re making ends meet… but just barely. I feel for them, and worry about them often. This economic “downturn” has been devastating to a lot of folks. Read some of their stories.

Yeah, right…

I think LinkedIn needs some tuning… because it’s completely busted.

Yeah, right.

Not a chance.

Moved to Linode

I recently made the switch from SliceHost to Linode. I made the switch because RackSpace bought SliceHost, and they’re moving everyone to this cloud computing model. While the talk is that it will cost me less—and I believe it likely does—I hate the fact that I feel like I’m being nickeled and dimed. Moreover, if I had to pay for everything I had now (the bandwidth, the storage, the cpu time), it would cost me more.

As a result, I made the switch to Linode. I looked around at several other places, a couple of which were cheaper. But at the end of the day, Linode was the only one that had all the features I wanted, and the reputation to go with it—how many companies do you know that pass the savings down to the users?1. To be honest, if you compare the setup, I get more from Linode for the same price at SliceHost… Linode is very competitive. So I went with Linode.

It was easy to sign up and get started. I ordered a node in their New Jersey datacenter, but I did test the bandwidth to a couple of their datacenters. They were all fast and finished the 100MB download within 100 milliseconds of each other. It was painless to get the node up and running, and it didn’t take long to transfer all the data over. The only hiccup was mine. I wanted to get rid of the Gallery2 instance, and host my website slightly differently. Those changes took about a day. My wife and I tested them out for a couple of days, and then pulled the trigger and changed the DNS to point to the new server.

If you’re shopping around for a new VPS provider, consider Linode. So far, the server has been more responsive, the control panel is awesome, and the few question I asked tech support came back with answers quickly. If you do sign-up, please use a link from this article. It contains a referral code and will give me a discount on my hosting expenses.


  1. Here are a few posts where Linode has passed on the savings: “Linode reduces transfer pricing“, “Linode turns 8; Disk space +25%“, and “Linode Turns 7, BIG RAM Increase“.  

Choosing a New Version Control System

The last several years have brought a frenzy of activity on the version control front. Subversion has been joined by Mercurial, Bazaar, Git, Monotone, Darcs, and many more. With all of them touting their abilities over the other, it’s become confusing to pick which one is right for you and your company. I’ve spent the last year and a half evaluating options for my company, watching the development of many of the projects, the community interactions, the feature set, the issues, and generally pushing the boundaries of what I could do with a subset of them.

For the record: I am involved with Subversion development, although to a much lesser degree these days. Some may see this as a bias, but I think it gives me insight into the tool and what it’s really capable of.

Continue reading Choosing a New Version Control System

Another set of pics…

It’s been a while since my last post. The holiday season always seems to get so busy that it’s hard to focus on anything else.

I’ve been meaning to do a post on LR3… but have been focused on version control things as of late, so I haven’t had much time to put the post together. I am almost finished with one on how I came to choose what we’re going to use next for version control at my company. Hopefully, that will arrive before March does.

In the mean time, I’ve been relaxing by doing a little more photography. Finally bought some studio lights, and radio triggers. I think I need to build some flags to help keep the light where I want it. I also want to find some white seamless paper in the area, so that I can avoid the insane shipping charges.

One decision that I’ve been struggling with is which lens to purchase next. Well, I finally purchased the 24-70mm f/2.8 after much turmoil. I was really hoping the 24-120mm f/4 was going to have a little less distortion, but it ended up being just a bit too much for tastes. Don’t get me wrong, it was a nice lens, but at a $1300 price tag, I expected more.

Here are some of the latest pictures I’ve taken. Enjoy!

RC Car

The Carousel

Ice!

Ice!

Fractured Candle

My young padawan

Ya Baby!

A few more pics…

I haven’t put any of my latest pics on here lately, so I figured I’d grab a few of my favorites. If you want to see more, head over to my Flickr stream. I’m still primarily using my 50mm f/1.8 lens, but some of these were taken with a 24-70mm f/2.8 that I rented, or a 100mm f/2.8 (fully manual) lens that I bought for $70. I hope to add another lens to my small collection soon. Still not sure which one, but but it’s more than likely to be the 24-120mm f/4 Nikon has just released, or the 24-70mm f/2.8.

BTW, I’m hoping to do a post on Lightroom 3 in the next month or so. As a preview, LR3 is definitely much faster than LR2, and the publishing features are very nice.

My bike is better than yours!

Moonbounce!

Sandbox!

Catch!

People Watching...

Hyatt Regency Atlanta

Disney's Hollywood Studios

Oracle vs Google

I’m not going to say much on this topic, except that I think software patents should die. However, Charlie Nutter—a fellow that I met at one of Python conferences—has quite a bit to say, and it’s an interesting viewpoint considering that he worked at Sun for a while helping to move along the JRuby implementation. He’s incredibly smart, and did a good job of summarizing the patents that Oracle is claiming Google violated with Android. So I’m just going to point you there for the details: http://blog.headius.com/2010/08/my-thoughts-on-oracle-v-google.html

Communications of the ACM

I joined the ACM about a year and a half ago, and they started sending me their equivalent of the IEEE Spectrum—called Communications of the ACM. For anyone that knows me, I’ve been quite vocal about the clear problem regarding multithreaded programming, and many of the issues that come with it. One area in particular is that the threading model is really the assembly language of concurrency and parallelism. It’s far too low low-level to constantly be thinking about. We need higher level constructions, but anything we do with the current languages is really insufficient. I’ve long felt the best way to approach this problem is from a language perspective, where the model can be completely different (Erlang, Scala, and Clojure are all examples of what can be done).

This month in the Communications of the ACM, there is a great article that goes into much more depth it all: Memory Models: A Case for Rethinking Parallel Languages and Hardware. It was a great read, and went into the nitty-gritty about where the problems are, the choices we can make, and even provided general direction for the future. If you’ve felt that multi-threaded programming was hard, but couldn’t lay your finger on why, read the article. If you want to understand how some of your current languages really work, and the trade-offs they made—or didn’t make… I’m looking at you C and C++—to deal with parallelism and concurrency, read the article. This stuff is really near and dear to my heart because I see the results of having no memory model. I’ve painfully debugged numerous threading-related issues, and know how the hardware and compiler can work against you in this context.

Over the past several years, I’ve slowly been learning the tools of the language trade. Partly because I’ve always been fascinated by languages and compilers, and partly because I’d like to help contribute to a solution to this problem one day. I think parallelism and concurrency are both to hard to think about with our current tools, and we need better support for doing the things programmers need to do every day. Computing has come a long way, and to a youngish person like myself, often feels as old as math. But when you get down to it, computer science is still extremely young. It really shows when you start talking about dealing with parallelism. We’ve only begun to explore the possibilities. It’ll be amazing to see how this field changes in the next 20 years.

While your on CACM’s website, check out An Interview With Edsger W. Dijkstra, one of the greatest influences on computer science. And take a look at The Singularity System, an article on Microsoft’s Singularity project. Singularity is one of the more interesting projects I’ve seen when it comes to advancing operating systems.

bzr-svn, round 2

In a previous post, I spoke at length how to get started using bzr-svn as a client against a Subversion repository. After another 8 months of using it, I have a few more thoughts I’d like to share.

A word of warning: much of this is about the negatives of using Bazaar against a Subversion repository. I’d like to be clear on a couple of points.

The issues raised are born out of the differences between Bazaar and Subversion. Bazaar is not Subversion. Bazaar made different choices about it’s model for version control, and bzr-svn does it’s best to bridge the gap.

I’d also like to take a moment, and say that these issues in no way reflects on bzr-svn’s author, Jelmer Vernooij. Jelmer has been extremely responsive to my inquires, quick to react to any branches I’ve proposed, and generous with his time and knowledge. I personally believe Jelmer is a fantastic programmer, and his prolific contributions to many open source projects is nothing less than astounding—I really don’t know how he finds the time.

Continue reading bzr-svn, round 2

Connecting to a safe@office vpn on a Mac…

Let me start off by saying that I started using a Mac several years ago because I finally reached a tipping point. I knew I wanted to get more into photography and have access to things like Lightroom and Photoshop. I was also getting tired of administering my box (although I’m happy that Ubuntu is largely making that practice disappear for the average user). I want to write code, do photography, and still participate in the business world without worrying about whether my OpenOffice document is going to render correctly.

So I made the switch (please don’t flame me, I still use Linux quite a bit, and that will never change). Since the change, I’ve not had to spend much time trying to configure my system. Everything largely Just Works.

That is, until you want to setup a VPN connection.

On the other end of the connection is a Safe@Office product. It claims to support a generic L2TP client, and until recently, I didn’t really believe that. I’ve tried to use Apple’s built-in client several times in the past, and no dice. It would connect, but I couldn’t do shit. I finally sat down to figure it out the other day because I’ve grown tired of IPSecuritas.

Turns out my problem boiled down to routing. I left the “Route all my data through the VPN” box unchecked (because that’s just ridiculous), and what I ended up with was an extra default route to the VPN gateway via the VPN tunnel and another route to the VPN gateway via my normal internet connection. The problem is that the assigned IP address (from the VPN) is not on the company’s subnet. What I needed was an extra route to say route those IP addresses over the VPN link. But I only need this when the VPN is active.

Easier said than done.

So, I confess, I’m not terribly knowledgeable about all the services that underly Mac OS X. I don’t really want to be that knowledgeable about them—that’s the appeal. To someone else out there, this may be common knowledge, but it took me a while to find the answer.

As I mentioned before, what I really needed was to set this route up when my VPN connection comes online. It turns out that this is entirely possible. When you establish your connection, there’s a service called pppd that is used to help tunnel traffic over your new IPSEC connection. If you check out the man page for it (man pppd from the command line), and scroll through 40 pages of options, you’ll find that it will invoke a script called /etc/ppp/ip-up when the connection comes up, and one called /etc/ppp/ip-down when the connection comes down. Further more, it will look in /etc/ppp/peers/ConnectionName for additional options.

Sweet!

So, reading a little more, /etc/ppp/ip-up is executed with several parameters:

interface-name tty-device speed local-IP-address remote-IP-address ipparam

interface-name is the name of the ppp interface that has been established. In my case, it’s ppp0. tty-device is the name of the device used to help establish the connection (think modem or serial port). That doesn’t apply here, so it’s just an empty string. speed is the speed the tty-device is running at, but we don’t have one. So, the speed is 0. The local-IP-address is the IP address it used for the local side of the connection. Unless you tell it otherwise, it’s the first primary ip address of your machine. The remote-IP-address is where it gets interesting. That’s the address of you on the other side of the tunnel. It’s also the one that doesn’t map into my company’s subnet, necessitating the additional route. Finally, there is ipparam. Turns out, there is an option called ipparam that can be used to control the value of this field. Awesome.

On the routing side, I need to add a route that goes to the company’s subnet, but forces it to do so over the tunnel. Mac’s route command is pretty similar to Linux’s, so after taking a look at the man page for route, I came up with the following command to add the route:

route -n add -net $NET -interface $IFNAME

$IFNAME is actually set by pppd when it calls /etc/ppp/ip-up and /etc/ppp/ip-down. It’s also the first parameter to the script, but I like using names. To delete it is just the opposite:

route -n delete -net $NET -interface $IFNAME

Were almost there.

Before I show you all the steps, let me say that the below steps only work for Snow Leopard. Turns out Apple broke this facility in Leopard, and it doesn’t appear to have ever been fixed (grrr).

Step 1: Create the VPN connection

I won’t walk through the details here. I created a new VPN connection called “Work” that uses L2TP. I also clicked the authentication button to and added my preshared key, and my password for the connection. On the Safe@Office side, you need to make sure you have enabled L2TP clients and set the preshared key. You also need to make sure you set up a user who is capable of using the VPN. This is fairly well documented online and in the manual, so I won’t repeat that here.

Step 2: Create an option file for the connection

My company, like most, uses the 10.0.0.0 class A subnet. I want to pass this as the ipparam to /etc/ppp/ip-up. Also, I called my connection “Work”. You might want to call yours something else, so translate accordingly. Here’s how to do it:

> sudo mkdir /etc/ppp/peers
> echo ipparam 10 | sudo tee /etc/ppp/peers/Work

This creates a file called /etc/ppp/peers/Work that contains a single line:

ipparam 10

Step 3: Create the ip-up and ip-down scripts

Next, I created the /etc/ppp/ip-up script with the following content:

#!/bin/bash

# This is the ipparam that we set in /etc/ppp/peers/Work
MYNET=$6

/sbin/route -n add -net $MYNET -interface $IFNAME >> /var/log/ppp.log 2>&1

And, the /etc/ppp/ip-down script:

#!/bin/bash

# This is the ipparam that we set in /etc/ppp/peers/Work
MYNET=$6

/sbin/route -n delete -net $MYNET -interface $IFNAME >> /var/log/ppp.log 2>&1

Remember to:

> sudo chmod a+x /etc/ppp/ip-up /etc/ppp/ip-down

That will make the files executable.

Step 4: Try it out!

Now when you connect to your VPN, it should add the necessary route and delete it when the connection goes down. If you’re having trouble, it’s likely because your network layout is different. Talk to the appropriate admin and get advice on what it should be from them.

Good luck!

Knswledgable?

C’mon on Apple! Really? Knswledgable?

Screenshot of 'knswledable' as a suggested spelling.

bzr-keychain

I’ve been meaning to look at this, but haven’t had the time until recently. bzr-keychain provides the ability to save your credentials into Mac OS X’s keychain. The trick is getting it in there in the first place.

Step 1: Get bzr-keychain

It’s simple:

cd ~/.bazaar/plugins
bzr branch lp:bzr-keychain keychain

Since this plugin requires a C extension module to access the keychain, you also need to run the following from ~/.bazaar/plugins/keychain:

python setup.by build_ext --inplace

Step 2: Mark up authentication.conf

We need to communicate to bzr that we want to look up a password for a server from the keychain. That means adding a section like this to ~/.bazaar/authentication.conf.

[Example]
scheme = http
host = bzr.example.com
password_encoding = keychain

The section header can be anything you want. The import bits are picking the scheme, specifying the host, and setting the password encoding to keychain. The last bit is what tells bzr to lookup the password from Mac’s keychain. Note that my scheme is “http” even though I’m using bzr+http, because the underlying transport is “http”. If you want to know more about the various, look at bzr help authentication.

Step 3: Add your password to your keychain

I won’t go into detail about how you should set up your various keychains. Dave Dribin has a great article on setting up your keychain. If you don’t follow that guide, simply add the password to your login keychain.

Launch the Keychain Access application in Applications/Utilities. Pick the keychain you want to add the item too. For me, I’ve setup a special one called “secure” as in the article above.

Next, you need to create the keychain item. There’s + at the bottom of the right hand pane. Click it to create a new keychain item.

This is where you need to click to add a keychain item.

Name it as follows:

<scheme://host:port> realm

In my case, I named my keychain item:

<http://bzr.example.com:80> private area

Here’s what the dialog looks like all filled out:

Filled out keychain item dialog.

And here’s my main screen after I’ve added the new item:

Main screen after the new keychain item is added.

Step 4: Try it out

Go ahead and try to pull from the server. Keychain will ask if Bazaar is allowed to access that particular entry. Grant it access and you’re done!

Hope you found this helpful!

Progressing…

I guess I should just start a photography section on this site. :-) At any rate, I was looking over some pictures of the last year, and figured I’d share some. I feel like I’ve come a long way, but still have some big strides I need to make. This last year has been great for my photography. I’m still using my 50mm f/1.8… I just can’t decide what lens to get, or if I should invest my dollars in lighting, a better tripod, or something else. So, I’ll keep on trudging along until I figure it out. :-) What I have figured out is:

  • I love taking photos of kids. When you catch them engrossed in something, they have such an intense look of wonder. And when they’re cranky, it shows too. :-)

  • I really want to do more with lighting. In particular, I really like cinematic lighting… but I have no idea where to start.

  • I need to find a better way to organize my photos. My current way is haphazard at best (at least they’re all in Lightroom though!).

  • I need to either get on the DNG bandwagon or get off. I’m currently bringing in photos as DNG, and up until recently, was also embedding the RAW file in there too. It’s way too much room. I’m currently running around 140GB for about 11,000 pictures.

With that, here are some of my favorite photos over the last several months:

Baseball!

May I take your order?

Fun with leaves!

Pier

An Artist's Tools

Escalator at Camden Yards

Subversion is moving to ASF!

I’m late in the game on announcing this, but Subversion is moving to the ASF!. I think it’s awesome that Collab.net has put in the work necessary to grow such a thriving open source project. I, personally, think this move makes a lot of sense. The ASF has a long standing history in terms of protecting open source, already hosts a number of large projects that many companies are involved in, and I believe it will open the door for more committers to be involved.

Congratulations to Subversion and thanks to all the folks who are helping to make this move possible! Now to get my ICLA in…

Bazaar as a Subversion “super client”

Nearly a year ago, I read an article by Ben Collins-Sussman called A Mercurial “super client”. About a month or so prior to that, I had started playing with Bazaar and, in particular, bzr-svn. Since then, I have really wanted to write a similar article for Bazaar, but haven’t found the time. I’m happy to say, that both Bazaar and bzr-svn have come to a head, with Bazaar releasing 2.0 and bzr-svn releasing 1.0. So, it seems like the right time to get this done!

Continue reading Bazaar as a Subversion “super client”

Awesome

Saw this over on the Unified Python Planet: http://plope.com/smartest_guy_in_the_room

Great rant.

A few lessons learned while taking pics of a sunrise…

So, I learned a few things this weekend when trying to get some pictures of a sunrise, that I thought I’d share.

Get up early

I knew this, but didn’t realize quite how early. The paper called for sunrise at 6:53, so I figured 6:15 would be fine. Wrong. Make sure you’re out at the location an hour before that. I missed some great, vibrant pictures by not being there earlier. Moreover, I really wanted to get that early sunrise shot with a long shutter speed… it didn’t happen. :-(

The sun moves fast

It really does move quickly, so you’ll need to be quick on your feet in moving around trying to get the shot that you want.

Scope out the area ahead of time

This is tough to do with two young kids who really want you to play… but if you have the chance, get out to the location and scout around a little. I was near a pier and I ended up taking some shots that I wouldn’t have wasted my time on, if I had scouted ahead of time. The flip side is that I probably missed a few good shots, because I wasted my time on the bad ones. :-(

Learn to use manual mode

I’m still an amateur, so these things aren’t hitting quite as quickly as I like, but looking back I should have switched the camera to manual mode. That would have given me more consistent exposures, and more control over just how much I wanted to blow the highlights to get the detail.

Bring all pieces to your tripod

In particular, the piece that mounts to the bottom of the camera. :-) I usually keep mine attached to the tripod, but didn’t this time and ended up leaving it at home.

Keep trying

Despite the hiccups, I kept shooting, trying different angles, playing with the exposure, and tone. I learned a lot about how much I do and don’t know. And it was good to get out and just shoot without any other interruptions. With that said, here are a couple pics that made the final cut:

Pier

 

Pier 2

My first triptych…

I’ve been following isayx3 on Flickr for a while now, and every once in a while, he throws up an image like this:

The Strawberry Clogger

 

It’s absolutely stunning. It looks like something out of a food magazine. So I decided to try it for myself. Let me tell ya: it’s much harder than it looks. There are so many factors at play: color, lighting, the relationship of the pictures, the direction of the subject, etc. So I took a number of photos over the weekend, trying to catch the boys at play, and contemplating their surroundings, etc. I’m pretty happy with what came out:

Andrew

 

Thanks to isayx3 for all the inspiration. He’s an awesome photographer.

Older