Skip to content

Commit 9bf4b30

Browse files
authored
Merge pull request #526 from peter-evans/detached-head
feat: support checkout on a commit in addition to a ref
2 parents fc687f8 + da6a868 commit 9bf4b30

File tree

6 files changed

+264
-122
lines changed

6 files changed

+264
-122
lines changed

README.md

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,6 @@ Note that in order to read the step output the action step must have an id.
7373
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
7474
```
7575

76-
### Checkout
77-
78-
This action expects repositories to be checked out with `actions/checkout@v2`.
79-
80-
If there is some reason you need to use `actions/checkout@v1` the following step can be added to checkout the branch.
81-
82-
```yml
83-
- uses: actions/checkout@v1
84-
- run: git checkout "${GITHUB_REF:11}"
85-
```
86-
8776
### Action behaviour
8877

8978
The default behaviour of the action is to create a pull request that will be continually updated with new changes until it is merged or closed.
@@ -115,6 +104,7 @@ To use this strategy, set input `branch-suffix` with one of the following option
115104
### Controlling commits
116105

117106
As well as relying on the action to handle uncommitted changes, you can additionally make your own commits before the action runs.
107+
Note that the repository must be checked out on a branch with a remote, it won't work for [events which checkout a commit](docs/concepts-guidelines.md#events-which-checkout-a-commit).
118108

119109
```yml
120110
steps:

__test__/create-or-update-branch.int.test.ts

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import {createOrUpdateBranch, tryFetch} from '../lib/create-or-update-branch'
1+
import {
2+
createOrUpdateBranch,
3+
tryFetch,
4+
getWorkingBaseAndType
5+
} from '../lib/create-or-update-branch'
26
import * as fs from 'fs'
37
import {GitCommandManager} from '../lib/git-command-manager'
48
import * as path from 'path'
@@ -193,6 +197,21 @@ describe('create-or-update-branch tests', () => {
193197
expect(await tryFetch(git, REMOTE_NAME, NOT_EXIST_BRANCH)).toBeFalsy()
194198
})
195199

200+
it('tests getWorkingBaseAndType on a checked out ref', async () => {
201+
const [workingBase, workingBaseType] = await getWorkingBaseAndType(git)
202+
expect(workingBase).toEqual(BASE)
203+
expect(workingBaseType).toEqual('branch')
204+
})
205+
206+
it('tests getWorkingBaseAndType on a checked out commit', async () => {
207+
// Checkout the HEAD commit SHA
208+
const headSha = await git.revParse('HEAD')
209+
await git.exec(['checkout', headSha])
210+
const [workingBase, workingBaseType] = await getWorkingBaseAndType(git)
211+
expect(workingBase).toEqual(headSha)
212+
expect(workingBaseType).toEqual('commit')
213+
})
214+
196215
it('tests no changes resulting in no new branch being created', async () => {
197216
const commitMessage = uuidv4()
198217
const result = await createOrUpdateBranch(
@@ -1450,4 +1469,133 @@ describe('create-or-update-branch tests', () => {
14501469
await gitLogMatches([_commitMessage, INIT_COMMIT_MESSAGE])
14511470
).toBeTruthy()
14521471
})
1472+
1473+
// Working Base is Not a Ref (WBNR)
1474+
// A commit is checked out leaving the repository in a "detached HEAD" state
1475+
1476+
it('tests create and update in detached HEAD state (WBNR)', async () => {
1477+
// Checkout the HEAD commit SHA
1478+
const headSha = await git.revParse('HEAD')
1479+
await git.checkout(headSha)
1480+
1481+
// Create tracked and untracked file changes
1482+
const changes = await createChanges()
1483+
const commitMessage = uuidv4()
1484+
const result = await createOrUpdateBranch(
1485+
git,
1486+
commitMessage,
1487+
BASE,
1488+
BRANCH,
1489+
REMOTE_NAME,
1490+
false
1491+
)
1492+
expect(result.action).toEqual('created')
1493+
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
1494+
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
1495+
expect(
1496+
await gitLogMatches([commitMessage, INIT_COMMIT_MESSAGE])
1497+
).toBeTruthy()
1498+
1499+
// Push pull request branch to remote
1500+
await git.push([
1501+
'--force-with-lease',
1502+
REMOTE_NAME,
1503+
`HEAD:refs/heads/${BRANCH}`
1504+
])
1505+
1506+
await afterTest(false)
1507+
await beforeTest()
1508+
1509+
// Checkout the HEAD commit SHA
1510+
const _headSha = await git.revParse('HEAD')
1511+
await git.checkout(_headSha)
1512+
1513+
// Create tracked and untracked file changes
1514+
const _changes = await createChanges()
1515+
const _commitMessage = uuidv4()
1516+
const _result = await createOrUpdateBranch(
1517+
git,
1518+
_commitMessage,
1519+
BASE,
1520+
BRANCH,
1521+
REMOTE_NAME,
1522+
false
1523+
)
1524+
expect(_result.action).toEqual('updated')
1525+
expect(_result.hasDiffWithBase).toBeTruthy()
1526+
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
1527+
expect(await getFileContent(UNTRACKED_FILE)).toEqual(_changes.untracked)
1528+
expect(
1529+
await gitLogMatches([_commitMessage, INIT_COMMIT_MESSAGE])
1530+
).toBeTruthy()
1531+
})
1532+
1533+
it('tests create and update with commits on the base inbetween, in detached HEAD state (WBNR)', async () => {
1534+
// Checkout the HEAD commit SHA
1535+
const headSha = await git.revParse('HEAD')
1536+
await git.checkout(headSha)
1537+
1538+
// Create tracked and untracked file changes
1539+
const changes = await createChanges()
1540+
const commitMessage = uuidv4()
1541+
const result = await createOrUpdateBranch(
1542+
git,
1543+
commitMessage,
1544+
BASE,
1545+
BRANCH,
1546+
REMOTE_NAME,
1547+
false
1548+
)
1549+
expect(result.action).toEqual('created')
1550+
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
1551+
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
1552+
expect(
1553+
await gitLogMatches([commitMessage, INIT_COMMIT_MESSAGE])
1554+
).toBeTruthy()
1555+
1556+
// Push pull request branch to remote
1557+
await git.push([
1558+
'--force-with-lease',
1559+
REMOTE_NAME,
1560+
`HEAD:refs/heads/${BRANCH}`
1561+
])
1562+
1563+
await afterTest(false)
1564+
await beforeTest()
1565+
1566+
// Create commits on the base
1567+
const commitsOnBase = await createCommits(git)
1568+
await git.push([
1569+
'--force',
1570+
REMOTE_NAME,
1571+
`HEAD:refs/heads/${DEFAULT_BRANCH}`
1572+
])
1573+
1574+
// Checkout the HEAD commit SHA
1575+
const _headSha = await git.revParse('HEAD')
1576+
await git.checkout(_headSha)
1577+
1578+
// Create tracked and untracked file changes
1579+
const _changes = await createChanges()
1580+
const _commitMessage = uuidv4()
1581+
const _result = await createOrUpdateBranch(
1582+
git,
1583+
_commitMessage,
1584+
BASE,
1585+
BRANCH,
1586+
REMOTE_NAME,
1587+
false
1588+
)
1589+
expect(_result.action).toEqual('updated')
1590+
expect(_result.hasDiffWithBase).toBeTruthy()
1591+
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
1592+
expect(await getFileContent(UNTRACKED_FILE)).toEqual(_changes.untracked)
1593+
expect(
1594+
await gitLogMatches([
1595+
_commitMessage,
1596+
...commitsOnBase.commitMsgs,
1597+
INIT_COMMIT_MESSAGE
1598+
])
1599+
).toBeTruthy()
1600+
})
14531601
})

dist/index.js

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2932,10 +2932,30 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
29322932
});
29332933
};
29342934
Object.defineProperty(exports, "__esModule", { value: true });
2935-
exports.createOrUpdateBranch = exports.tryFetch = void 0;
2935+
exports.createOrUpdateBranch = exports.tryFetch = exports.getWorkingBaseAndType = exports.WorkingBaseType = void 0;
29362936
const core = __importStar(__webpack_require__(186));
29372937
const uuid_1 = __webpack_require__(840);
29382938
const CHERRYPICK_EMPTY = 'The previous cherry-pick is now empty, possibly due to conflict resolution.';
2939+
var WorkingBaseType;
2940+
(function (WorkingBaseType) {
2941+
WorkingBaseType["Branch"] = "branch";
2942+
WorkingBaseType["Commit"] = "commit";
2943+
})(WorkingBaseType = exports.WorkingBaseType || (exports.WorkingBaseType = {}));
2944+
function getWorkingBaseAndType(git) {
2945+
return __awaiter(this, void 0, void 0, function* () {
2946+
const symbolicRefResult = yield git.exec(['symbolic-ref', 'HEAD', '--short'], true);
2947+
if (symbolicRefResult.exitCode == 0) {
2948+
// A ref is checked out
2949+
return [symbolicRefResult.stdout.trim(), WorkingBaseType.Branch];
2950+
}
2951+
else {
2952+
// A commit is checked out (detached HEAD)
2953+
const headSha = yield git.revParse('HEAD');
2954+
return [headSha, WorkingBaseType.Commit];
2955+
}
2956+
});
2957+
}
2958+
exports.getWorkingBaseAndType = getWorkingBaseAndType;
29392959
function tryFetch(git, remote, branch) {
29402960
return __awaiter(this, void 0, void 0, function* () {
29412961
try {
@@ -2983,8 +3003,14 @@ function splitLines(multilineString) {
29833003
}
29843004
function createOrUpdateBranch(git, commitMessage, base, branch, branchRemoteName, signoff) {
29853005
return __awaiter(this, void 0, void 0, function* () {
2986-
// Get the working base. This may or may not be the actual base.
2987-
const workingBase = yield git.symbolicRef('HEAD', ['--short']);
3006+
// Get the working base.
3007+
// When a ref, it may or may not be the actual base.
3008+
// When a commit, we must rebase onto the actual base.
3009+
const [workingBase, workingBaseType] = yield getWorkingBaseAndType(git);
3010+
core.info(`Working base is ${workingBaseType} '${workingBase}'`);
3011+
if (workingBaseType == WorkingBaseType.Commit && !base) {
3012+
throw new Error(`When in 'detached HEAD' state, 'base' must be supplied.`);
3013+
}
29883014
// If the base is not specified it is assumed to be the working base.
29893015
base = base ? base : workingBase;
29903016
const baseRemote = 'origin';
@@ -3009,10 +3035,14 @@ function createOrUpdateBranch(git, commitMessage, base, branch, branchRemoteName
30093035
}
30103036
// Perform fetch and reset the working base
30113037
// Commits made during the workflow will be removed
3012-
yield git.fetch([`${workingBase}:${workingBase}`], baseRemote, ['--force']);
3038+
if (workingBaseType == WorkingBaseType.Branch) {
3039+
core.info(`Resetting working base branch '${workingBase}' to its remote`);
3040+
yield git.fetch([`${workingBase}:${workingBase}`], baseRemote, ['--force']);
3041+
}
30133042
// If the working base is not the base, rebase the temp branch commits
3043+
// This will also be true if the working base type is a commit
30143044
if (workingBase != base) {
3015-
core.info(`Rebasing commits made to branch '${workingBase}' on to base branch '${base}'`);
3045+
core.info(`Rebasing commits made to ${workingBaseType} '${workingBase}' on to base branch '${base}'`);
30163046
// Checkout the actual base
30173047
yield git.fetch([`${base}:${base}`], baseRemote, ['--force']);
30183048
yield git.checkout(base);
@@ -6927,19 +6957,14 @@ function createPullRequest(inputs) {
69276957
yield gitAuthHelper.configureToken(inputs.token);
69286958
core.endGroup();
69296959
}
6930-
// Determine if the checked out ref is a valid base for a pull request
6931-
// The action needs the checked out HEAD ref to be a branch
6932-
// This check will fail in the following cases:
6933-
// - HEAD is detached
6934-
// - HEAD is a merge commit (pull_request events)
6935-
// - HEAD is a tag
6936-
core.startGroup('Checking the checked out ref');
6937-
const symbolicRefResult = yield git.exec(['symbolic-ref', 'HEAD', '--short'], true);
6938-
if (symbolicRefResult.exitCode != 0) {
6939-
core.debug(`${symbolicRefResult.stderr}`);
6940-
throw new Error('The checked out ref is not a valid base for a pull request. Unable to continue.');
6960+
core.startGroup('Checking the base repository state');
6961+
const [workingBase, workingBaseType] = yield create_or_update_branch_1.getWorkingBaseAndType(git);
6962+
core.info(`Working base is ${workingBaseType} '${workingBase}'`);
6963+
// When in detached HEAD state (checked out on a commit), we need to
6964+
// know the 'base' branch in order to rebase changes.
6965+
if (workingBaseType == create_or_update_branch_1.WorkingBaseType.Commit && !inputs.base) {
6966+
throw new Error(`When the repository is checked out on a commit instead of a branch, the 'base' input must be supplied.`);
69416967
}
6942-
const workingBase = symbolicRefResult.stdout.trim();
69436968
// If the base is not specified it is assumed to be the working base.
69446969
const base = inputs.base ? inputs.base : workingBase;
69456970
// Throw an error if the base and branch are not different branches

0 commit comments

Comments
 (0)