We’ve all been there—staring at a function that looks like it was written by an over-caffeinated goblin at 3 AM (maybe even your alter ego). You could pretend it doesn’t exist, or you could refactor it. Luckily, GitHub Copilot makes the second option less painful.
Let’s get to it.
What is code refactoring?
Feel free to breeze past this section if you already know what’s involved with refactoring code, but we wanted to take a moment to cover what we’ll be looking at today.
Think of refactoring as giving your project some digital spring cleaning—a glow up for your functions, classes, and modules. But instead of just tidying up, you’re making your code more efficient, maintainable, and readable, all without changing its external behavior.
Some standard ways of refactoring include:
Simplifying complex conditionals (because no one should need a PhD to read your if statements)
Extracting duplicated logic (so you’re not trying to maintain code in ten different places)
Improving variable and function names (because doThing() is a crime against humanity)
Converting monolithic functions into smaller, modular pieces (to prevent the dreaded “function that spans multiple screens” scenario)
Refactoring isn’t just about tidiness—it’s about making your codebase more resilient, scalable, and enjoyable to work with. Let’s find out how GitHub Copilot can help you do it faster and with fewer headaches.
Know what your code does before you refactor anything
It might seem obvious to say this, but before you can refactor any code you need to understand how it works. If you don’t know what your code is doing, you won’t know whether or not the “improvements” you’re making are changing the core functionality of the code.
Consider the following method:
public String getSound(String animal) {
if (animal == null) {
System.out.println("Oops! A null animal?");
} else if (animal.equalsIgnoreCase("Dog")) {
return "Bark";
} else if ( animal.equalsIgnoreCase("Cat")) {
return "Meow";
} else if ( animal.equalsIgnoreCase("Bird")) {
return "Tweet";
}
return "Unknown";
}
You might look at this and immediately think “they should use a switch statement,” and that would be one example of refactoring the code. But having that knee jerk reaction requires you to know how if-statements and switch-statements work. You can only make that suggestion if you understand this code will continue cycling through if-statements until it finds an appropriate match. Otherwise, it will return the value of Unknown.
As codebases get more complex and make calls between multiple files, this gets (much) more complicated. And this is one way Copilot can help you.
You can ask Copilot Chat to explain how some code works, either by asking in plain language or using the /explain slash command. To limit the scope of what Copilot looks at, select the code in your IDE before asking your query, or specify specific files for it to consider by using #file. While you’re at it, you can even ask it to add code comments to help you (or anyone else reading the code) in the future.
Here are some sample prompts:
Explain what this code does.
What is this code doing?
Add comments to this code to make it more understandable.
You should use Copilot Chat to analyze and explain your codebase until you fully understand the code you’re looking to refactor.
Try some blanket improvements to refactor your code
Like most things in life, it’s usually best to start small. When you’re first getting started with refactoring code, keep it simple: open up Copilot Chat in your project and ask “how would you improve this?” Just like when you are asking GitHub Copilot to explain some code, you can specify what it looks at by highlighting sections of code or identifying specific files by using #file.
Here are some sample prompts:
How would you improve this?
Improve the variable names in this function.
#file:pageInit.js, #file:socketConnector.js Offer suggestions to simplify this code.
Copilot will then offer suggestions to improve the code in the way that you specified. This is great for getting started, but Copilot can do much more if you give it some guidance.
When working with any generative AI-powered developer tool, it is often useful to include more context in your prompts—ones that are more likely to get you the results you’re looking for. By being specific about what you want the tool to do, it focuses the efforts toward that end.
It’s a bit like if someone tells you to code something, and you have so many ideas and questions that you’re not quite sure where to begin. But if someone tells you they specifically want you to code a class that sorts a list, you can focus on the task at hand.
The prompts above don’t offer much specificity or context, which leaves Copilot to explore all the possible ways your code could be improved. The upside? You may see options that you might not have considered. The downside is some of the proposed solutions might not address your specific concerns.
Make a plan for refactoring your codebase
What do you want to do to your code? Do you want to make it more readable? Or do you want to find redundant code and remove it? Coming up with a plan for the improvements you want to make will help you to hit your goals. This comes back to making sure you understand your code. If you know how it works, you can come up with a plan for the type of improvements that you want to make.
Maybe your code base has a bunch of different scripts that all perform the same general function. You could strip out the code into a common module to import into each of the different scripts, making the code easier to change and maintain.
To do so, you can direct Copilot to look for these common code sections and to pull them into a single module.
Here’s a sample prompt:
Inspect all my js files for GitHub API calls and create a new class that will manage all the GitHub API calls.
Now that we have provided some guidance and additional context, Copilot will provide suggestions targeting this specific improvement that we want to make to our code.
You can also provide a laundry list of tasks, or ask Copilot to keep things in mind while it is doing the refactoring.
In that vein, here’s a sample prompt to consider:
Can you refactor the GitHubController class to:
- remove nested logic structures
- make the code more concise
- while doing this, check if the code is safe and add comments if not
The takeaway here is Copilot is very good at taking directions—and the more specific your directions are, the more it will generate outputs in line with your intended end goal.
A real-world example: Using GitHub Copilot to refactor code
Sometimes the best way to understand something is to see it in use. To help with this, we wanted to share an experience where we needed to use GitHub Copilot to refactor our own code, interspersed with some general guidance and pointers.
It started with a couple of organizations independently requesting help with their individual large migration projects, which involved moving from other data centers into tens of thousands of repositories on GitHub. After building a proof of concept, we started creating individual systems, each tailored to the needs of the individual customer and their unique systems. While we were doing this, a third organization made a similar request for help, and we realized this was a common need.
At this point in time, we had two systems that were both suited for large-scale migrations, but each of them was heavily customized for the original company making the request. We quickly realized the best solution involved refactoring our code to improve its modularity so we could use it across different organizations. This would not be easy—the codebase was made up of several different scripts and workflows with a lot of moving parts. And that’s when we turned to Copilot for help.
First we came up with a plan. We created a set of high-level goals and used these to guide us during the refactoring process. We wanted to make sure the code was:
Modular and reusable for different organizations: The code should be organized into small, self-contained modules that could be reused and combined for new functionality.
Maintainable: The code should be easy to maintain and extend as needed. That meant making it well-organized, well-documented, and easy to understand.
Customizable for different organizations: The code should be flexible enough to meet the unique needs of each organization.
Following coding standards and best practices: The code should be consistent, readable, and easy to understand.
With this plan in place, it was time to ask Copilot for help.
Step 1: Creating the initial module
Our first step was creating a common module we could import into each of the different scripts. This would allow us to reuse the same code across many different scripts, making it easier to maintain and extend. We created a skeleton of a module called gh-migrations. Then we used Copilot to fill in the details.
Here was the prompt we gave Copilot:
I am creating a module, gh-migrations, which will hold common functionality to be used through out my project. I have already created an index.js for the module. Please inspect the other js files for GitHub API calls, create a new class for the module which will manage all GitHub API calls. This class should also account for rate limiting and errors. Do not make edits to the javascript files, rather identify the API calls, and add the functionality to the new GitHub API class.
And here was Copilot’s response:
I'll help create a GitHub API management class based on the API calls found in the provided files. Here's a step-by-step solution:
1. From analyzing the files, I identified these common GitHub API operations:
* Issues creation and updates
* Issue search
* Rate limit handling (403 errors)
* Common error handling patterns
2. I'll create a class that encapsulates these operations with proper error handling and rate limiting.
.github/lib/gh-migrations/index.js
Here's the GitHub API management class implementation:
<code>
This implementation:
1. Creates a GitHubApiManager class that handles all GitHub API interactions
2. Implements automatic rate limiting retry logic in withRateLimit
3. Provides methods for common operations:
* createIssue: Creates a new issue
* updateIssue: Updates an existing issue
* searchIssues: Searches for issues using GitHub's search API
* findIssueByTitle: Helper method to find issues by title
* getIssue: Gets a specific issue by number
Usage example:
<code>
The class handles:
* Automatic retry on rate limits
* Consistent error handling
* Repository context management
* Common GitHub API operations used across the codebase
This centralizes all GitHub API interactions and makes the code more maintainable and consistent.
After reviewing the code (as you should always do!), we made the recommended changes. The next step was putting these changes into a separate file while ensuring the index.js file could reference it.
Here was the prompt we entered for Copilot:
Rename the class GitHubApiManager to GHApi and put it in a separate file that is referenced in the index.js file.
And here was Copilot’s response:
I'll help you split the code into two files. Here's the step by step solution:
1. Create a new file gh-api.js for the GHApi class
2. Update index.js to export the renamed class from the new file
.github/lib/gh-migrations/gh-api.js
Create a new file containing the renamed class:
<code>
.github/lib/gh-migrations/index.js
Update the index file to export the renamed class from the new file:
<code>
The changes:
1. Created a new file gh-api.js with the renamed class
2. Moved all the class implementation to the new file
3. Updated the class name from GitHubApiManager to GHApi
4. Modified index.js to require and export the class from the new file
Now consumers of this module can continue to import it the same way, but the implementation is better organized.
After making these changes, we then had to repeat the process for each of the files that contained GitHub API calls. Be aware, we didn’t want to refactor the code directly at this stage. Instead, we wanted to create a common module we could use for refactoring code later.
Here was the prompt we entered for Copilot:
The gh-api.js file is a new class that will be used to manage all GitHub API calls. Please review the other javascript files for GitHub API calls to be added to gh-api.js. Do not edit the javascript files, only add new functionality to gh-api.js.
And here was Copilot’s response:
I'll analyze the JavaScript files and add the missing API functionality to the GHApi class.
.github/lib/gh-migrations/gh-api.js
Add methods for issue operations, searching, and deletion based on the API calls found in migration.js and application.js:
<code>
This implementation adds all the GitHub API functionality found in the other files:
1. From migration.js:
* createComment for updating issues with comments
* getIssue and isMigrationIssue for checking migration status
2. From application.js:
* searchIssues and searchIssuesByTitle for finding issues
* getIssueNodeId and deleteIssue for issue deletion via GraphQL
* findJiraIssue helper method to get Jira ticket information
The methods maintain the same rate limiting and error handling patterns while providing a cleaner interface for the API operations.
Step 2: Adding new classes
Once we had the new GHApi class to handle the GitHub API, we want to create some reusable classes for different entities in our application. Since our application is a migration tool managed via GitHub Issues, we wanted to create classes for the following entities:
Migration issues represent repository migrations from various sources. They contain configuration and metadata about the repository and how the migration should be performed.
Batch issues represent a batch of migrations. They contain more high-level metadata about groups of individual migration issues such as custom application ids and status of the batch.
Team issues represent the migration of a group of users, their associated repositories, and the permissions to those repositories.
Rewire issues represent Azure DevOps pipelines rewire operations. They allow for the reconfiguration of Azure DevOps pipelines to point to the new GitHub repositories after a migration has been completed.
Here was the prompt we entered for Copilot:
Review the create-migration-issue.js and develop a new class called MigrationIssue that contains the properties associated with a migration. This shoud be added to the gh-migrations module where index.js is located.
And here was Copilot’s response:
I'll help create a new MigrationIssue class based on the migration configuration and status shown in the code. Here's the step-by-step solution:
1. Create a new file migration-issue.js in the gh-migrations directory
2. Update the index.js file to expose the new class
3. Extract configuration and status properties from the create-migration-issues.js file into the new class
migration-issue.js
Create a new class to represent a migration issue with all necessary properties and methods.
<code>
index.js
Update the index.js to expose the new MigrationIssue class.
<code>
The new MigrationIssue class encapsulates all the properties that were previously spread across various objects in the create-migration-issues.js file. It provides a clean interface for creating and managing migration issues, with helper methods for generating issue titles and source types.
You can now use this class in create-migration-issues.js by replacing the manual object creation with the MigrationIssue class. For example, the createMigrationConfig function could be replaced with:
<code>
The process for creating new classes for the other entities was similar to the above. We created new classes for BatchIssues, TeamIssues, and RewireIssues, each with their own properties and methods.
Step 3: Encapsulation
After we built out our entity classes, we realized they should manage their own state. This is a common pattern in object-oriented programming, where the class is responsible for managing its state and behavior. Remember our goal about following good coding standards?
The class should include methods to get and set the state of the object, as well as methods to perform actions on the object. It should also be responsible for utilizing the GitHub API and manage the state of the object rather than the client code. This will help standardize and protect the state of the object, as well as give the client code a simpler interface to work with.
Here was the prompt we entered for Copilot:
The MigrationIssue class should contain a initializer that uses the gh-api.js to create the resulting GitHub Issue. The initializer should take in an optional parameter called issueNumber, if the issueNumber is omitted the initializer should create a new Migration Issue and return the issue number created, if the issueNumber is passed to the initializer, the gh-api.js should be used to return the title and body of the issue which the class will parse to create the properties of the MigrationIssue.
And here was Copilot’s response:
I'll help you modify the code to add initialization logic to the MigrationIssue class. Here's the step-by-step solution:
1. Add a static method to parse issue body into configuration
2. Add initialization methods to handle both new and existing issues
3. Modify constructor to accept GHApi instance
4. Add method to generate issue body
5. Add method to create or load issue
migration-issue.js
Update the MigrationIssue class to handle initialization from GHApi.
<code>
The main changes include:
1. Constructor now accepts a GHApi instance as first parameter
2. Added parseIssueBody static method to parse configuration from issue body
3. Added generateIssueBody method to create the issue body format
4. Added initialize async method that either creates a new issue or loads an existing one
5. The initialize method returns the issue number in both cases
To use this updated class:
<code>
Remember how we said it’s always important to review suggestions from Copilot? In this case, the initialize method wasn’t connected to the constructor, like we would expect. We asked Copilot to address this issue with an iterative prompt.
Here was the prompt we entered for Copilot:
The MigrationIssue class has an initialize function that encapsulates functionality that should be included in the constructor. Rewire the constuctor method to include the initialzie function.
And here was Copilot’s response:
Let's modify the class to move the initialization logic into the constructor. Here's the step-by-step solution:
1. Make constructors async
2. Move initialization logic into constructor
3. Remove separate initialize method
4. Keep static parseIssueBody method
migration-issue.js
Move initialization logic into constructor.
<code>
The main changes:
1. Removed initialize method from the class
2. Added static create and load factory methods
3. Added save method to handle issue creation
4. Constructor remains synchronous but now only handles property initialization
Usage example:
<code>
This approach follows better object-oriented principles by:
1. Using factory methods for different creation scenarios
2. Keeping the constructor simple and synchronous
3. Separating issue creation into a distinct operation
4. Making the API more intuitive
In this case, Copilot suggested a different approach than we were thinking of with a factory method (which is a great way to handle different scenarios when creating or loading an issue). Even if it wasn’t what we were thinking, it worked and also kept the constructor clean and focused on just initializing the object. Moreover, the save method is a good way to encapsulate the logic for creating a new issue, and it makes the code more readable.
Now we just needed to repeat this process with each of the new classes we added.
Step 4: Final refactoring
After we built a common module, we needed to refactor the client code to make it much cleaner and easier to read and maintain. After that, we’d also be able to identify and add any missing functionality in the common module.
This was an iterative process, and we needed to go back and forth between the client code and the common module to make sure everything worked properly. Because of the way this project evolved, it afforded us the opportunity to make sweeping changes to the codebase. It’s much more common to make small, incremental changes to the codebase, especially if it’s code that’s actively in use. In both of these cases, don’t forget to write tests to ensure the code works as expected.
Here was the prompt we entered for Copilot:
Using the gh-migrations module, refactor the create-migration-issues.js
And here was Copilot’s response:
I'll help you refactor the create-migration-issues.js file to use the gh-migrations module. Here's the step-by-step solution:
1. Import the gh-migrations module
2. Create GHApi instance
3. Use MigrationIssue class for creating and managing migration issues
4. Simplify the main logic using the new classes
create-migration-issues.js
Refactor to use the gh-migrations module for better code organization and reusability.
<code>
The refactoring:
1. Removes redundant code by using the MigrationIssue class
2. Simplifies issue creation and management
3. Uses GHApi for GitHub API calls with built-in rate limiting
4. Removes duplicate code for different source types
5. Improves code organization and maintainability
The main changes:
* Removed several helper functions as they're now part of the MigrationIssue class
* Simplified the main logic flow
* Added better error handling
* Used structured configuration object
* Maintained the same functionality while reducing code complexity
Now we needed to refactor the remaining scripts in the same manner. For example, this is how we used Copilot to help with refactoring the create-team-issues.js script.
Here was the prompt we entered for Copilot:
Using the gh-migrations module refactor the create-team-issues.js
And here was Copilot’s response:
I'll help you refactor the create-team-issues.js file to use the gh-migrations module. Here's the step-by-step solution:
1. Import the gh-migrations module
2. Replace manual issue creation with TeamIssue class
3. Simplify team creation logic using the new class
4. Maintain the same JiraIssue tracking functionality
create-team-issues.js
Refactor to use the gh-migrations module for creating team issues.
<code>
The refactoring:
1. Uses the TeamIssue class to manage team issue creation
2. Simplifies the code by removing redundant config and status objects
3. Uses GHApi for GitHub operations with built-in rate limiting
4. Maintains the same JiraIssue tracking functionality
5. Reduces code duplication by using a loop for team creation
The code is now more maintainable and follows the same pattern as the migration issues implementation.
From there, it was a matter of rinsing and repeating this process with each of our scripts.
By first creating the common module, we could guide Copilot and make sure that the final, refactored codebase was something we could use with multiple organizations. It gave us a foundation we could reuse and customize based on the needs of the system we were migrating.
Things to keep in mind
Sometimes refactoring code can be really easy, like our first example with the sequential if statements. But the more you dig into it, the more it grows, and you might find yourself facing down the Herculean task of trying to refactor a codebase that stretches across multiple files from a larger team of developers.
Speaking from experience: It can be daunting to even know where to start.
Remember to take a step back and start with the basics. Your first step should always be improving your understanding of the codebase you want to refactor. The more you know about what you’re starting out with, the greater chance that you will be able to think of ways to refactor it effectively. And once you come up with some ideas, you can always ask GitHub Copilot to help turn your vision into reality. It’s got your back and is more than willing to help.
Start using GitHub Copilot for free
Our free version of GitHub Copilot is included by default in personal GitHub accounts and VS Code to help you start new projects, manage existing repositories, and more.
Anthony Grutta is a Senior Service Delivery Engineer at GitHub, tackling enterprise developer challenges by day and conquering video game quests by night. His hacking skills are top-notch, but his culinary experiments often need a CTRL+Z. When he's not coding or scrambling eggs, you'll find him huffing and puffing through ambitious running routes. Anthony blends humor and expertise on the Expert Services team.
We share some of the GitHub Copilot team’s experience evaluating AI models, with a focus on our offline evaluations—the tests we run before making any change to our production environment.
We do newsletters, too
Discover tips, technical guides, and best practices in our biweekly newsletter just for devs.