Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package nebula.plugin.release

import nebula.test.IntegrationTestKitSpec
import spock.lang.Subject

@Subject(ReleasePlugin)
class ReleasePluginShallowCloneIntegrationSpec extends IntegrationTestKitSpec {

def 'shallow clone without unshallow flag produces default dev version'() {
given:
def origin = setupOriginRepoWithTag('1.0.0')
shallowClone(origin, projectDir, 1)
writeBuildFile()

when:
def results = runTasks('devSnapshot')

then:
results.output.contains('0.1.0-dev.')
}

def 'shallow clone with unshallowEnabled finds correct tag and infers proper version'() {
given:
def origin = setupOriginRepoWithTag('1.0.0')
shallowClone(origin, projectDir, 1)
writeBuildFile()
enableUnshallow()

when:
def results = runTasks('devSnapshot')

then:
results.output.contains('Shallow clone detected: deepening by 30 commits')
results.output.contains('Found version tag after deepening')
results.output.contains('1.0.1-dev.')
}

def 'shallow clone that already has tag within depth works without deepening'() {
given:
def origin = setupOriginRepoWithTagOnHead('2.0.0')
shallowClone(origin, projectDir, 1)
writeBuildFile()
enableUnshallow()

when:
def results = runTasks('devSnapshot')

then:
!results.output.contains('Shallow clone detected')
results.output.contains('2.0.0')
}

private File setupOriginRepoWithTag(String tagVersion) {
File origin = new File(projectDir.parent, "${projectDir.name}-origin")
if (origin.exists()) origin.deleteDir()
origin.mkdirs()

git(origin, 'init')
git(origin, 'checkout', '-b', 'master')
configureGitUser(origin)

new File(origin, 'initial.txt').text = 'initial'
git(origin, 'add', '.')
git(origin, 'commit', '-m', 'Initial commit')

// Create a tag
git(origin, 'tag', '-a', "v${tagVersion}", '-m', "Release ${tagVersion}")

// Add more commits after the tag so that shallow clone won't see it
(1..5).each { i ->
new File(origin, "file${i}.txt").text = "content ${i}"
git(origin, 'add', '.')
git(origin, 'commit', '-m', "Commit ${i} after tag")
}

return origin
}

private File setupOriginRepoWithTagOnHead(String tagVersion) {
File origin = new File(projectDir.parent, "${projectDir.name}-origin")
if (origin.exists()) origin.deleteDir()
origin.mkdirs()

git(origin, 'init')
git(origin, 'checkout', '-b', 'master')
configureGitUser(origin)

new File(origin, 'initial.txt').text = 'initial'
git(origin, 'add', '.')
git(origin, 'commit', '-m', 'Initial commit')

// Tag on the latest commit (HEAD)
git(origin, 'tag', '-a', "v${tagVersion}", '-m', "Release ${tagVersion}")

return origin
}

private void shallowClone(File origin, File targetDir, int depth) {
// Clean the target dir but keep it existing (IntegrationTestKitSpec needs it)
targetDir.listFiles()?.each {
if (it.isDirectory()) it.deleteDir() else it.delete()
}
def process = new ProcessBuilder('git', 'clone', '--depth', "${depth}", '--branch', 'master', origin.absolutePath, targetDir.absolutePath)
.redirectErrorStream(true)
.start()
def output = process.inputStream.text
process.waitFor()
if (process.exitValue() != 0) {
throw new RuntimeException("Failed to shallow clone: ${output}")
}
}

private void writeBuildFile() {
buildFile << """\
plugins {
id 'com.netflix.nebula.release'
id 'java'
}

ext.dryRun = true
group = 'test'

task showVersion {
doLast {
logger.lifecycle "Version in task: \${version.toString()}"
}
}
""".stripIndent()
new File(projectDir, '.gitignore') << '''.gradle-test-kit
.gradle
build/
gradle.properties'''.stripIndent()

configureGitUser(projectDir)
git(projectDir, 'add', '.')
git(projectDir, 'commit', '-m', 'Add build files')
}

private void enableUnshallow() {
new File(projectDir, "gradle.properties").text = "nebula.release.features.unshallowEnabled=true\n"
}

private static void configureGitUser(File dir) {
git(dir, 'config', 'user.email', 'test@example.com')
git(dir, 'config', 'user.name', 'Test User')
git(dir, 'config', 'commit.gpgsign', 'false')
git(dir, 'config', 'tag.gpgsign', 'false')
}

private static String git(File dir, String... args) {
def command = ['git'] + args.toList()
def process = new ProcessBuilder(command)
.directory(dir)
.redirectErrorStream(true)
.start()
def output = process.inputStream.text
process.waitFor()
if (process.exitValue() != 0) {
throw new RuntimeException("Git command failed: ${command.join(' ')}\n${output}")
}
return output
}
}
5 changes: 5 additions & 0 deletions src/main/groovy/nebula/plugin/release/FeatureFlags.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import org.gradle.api.Project
class FeatureFlags {
public static final String NEBULA_RELEASE_REPLACE_DEV_SNAPSHOT_WITH_IMMUTABLE_SNAPSHOT = "nebula.release.features.replaceDevWithImmutableSnapshot"
public static final String NEBULA_RELEASE_IMMUTABLE_SNAPSHOT_TIMESTAMP_PRECISION = "nebula.release.features.immutableSnapshot.timestampPrecision"
public static final String NEBULA_RELEASE_UNSHALLOW_ENABLED = "nebula.release.features.unshallowEnabled"

static boolean isDevSnapshotReplacementEnabled(Project project) {
return project.findProperty(NEBULA_RELEASE_REPLACE_DEV_SNAPSHOT_WITH_IMMUTABLE_SNAPSHOT)?.toString()?.toBoolean()
}

static boolean isUnshallowEnabled(Project project) {
return project.findProperty(NEBULA_RELEASE_UNSHALLOW_ENABLED)?.toString()?.toBoolean()
}

static TimestampPrecision immutableSnapshotTimestampPrecision(Project project) {
return project.hasProperty(NEBULA_RELEASE_IMMUTABLE_SNAPSHOT_TIMESTAMP_PRECISION) ?
TimestampPrecision.from(project.findProperty(NEBULA_RELEASE_IMMUTABLE_SNAPSHOT_TIMESTAMP_PRECISION).toString())
Expand Down
4 changes: 4 additions & 0 deletions src/main/groovy/nebula/plugin/release/ReleasePlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ class ReleasePlugin implements Plugin<Project> {
return
}

if (FeatureFlags.isUnshallowEnabled(project) && gitBuildService.get().isShallowRepository()) {
gitBuildService.get().deepenUntilTagFound('origin')
}

if (project == project.rootProject) {
// Verify user git config only when using release tags and 'release.useLastTag' property is not used
boolean shouldVerifyUserGitConfig = isReleaseTaskThatRequiresTagging(project.gradle.startParameter.taskNames) && !isUsingLatestTag(project)
Expand Down
53 changes: 53 additions & 0 deletions src/main/groovy/nebula/plugin/release/git/GitBuildService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import nebula.plugin.release.git.command.DescribeHeadWithTag
import nebula.plugin.release.git.command.DescribeHeadWithTagWithExclude
import nebula.plugin.release.git.command.EmailFromLog
import nebula.plugin.release.git.command.FetchChanges
import nebula.plugin.release.git.command.FetchDeepen
import nebula.plugin.release.git.command.GetGitConfigValue
import nebula.plugin.release.git.command.HeadTags
import nebula.plugin.release.git.command.IsCurrentBranchBehindRemote
import nebula.plugin.release.git.command.IsGitRepo
import nebula.plugin.release.git.command.IsShallowRepository
import nebula.plugin.release.git.command.IsTrackingRemoteBranch
import nebula.plugin.release.git.command.PushTag
import nebula.plugin.release.git.command.RevListCountHead
Expand Down Expand Up @@ -274,6 +276,57 @@ abstract class GitBuildService implements BuildService<GitBuildService.Params> {
}
}

/**
* Checks if the current repository is a shallow clone
* @return true if the repository is shallow
*/
boolean isShallowRepository() {
try {
def isShallowProvider = providerFactory.of(IsShallowRepository.class) {
it.parameters.rootDir.set(gitRootDir)
}
return Boolean.valueOf(isShallowProvider.get().toString())
} catch (Exception e) {
return false
}
}

/**
* Fetches additional history from a remote using --deepen
* @param remote
* @param depth number of commits to deepen
*/
void fetchDeepen(String remote, int depth) {
try {
providerFactory.of(FetchDeepen.class) {
it.parameters.rootDir.set(gitRootDir)
it.parameters.remote.set(remote)
it.parameters.depth.set(depth.toString())
}.get()
} catch (Exception e) {
LOGGER.warn("Failed to deepen clone from remote {}: {}", remote, e.message)
}
}

/**
* Incrementally deepens a shallow clone until a version tag is found
* @param remote the remote to fetch from
*/
void deepenUntilTagFound(String remote) {
int maxIterations = 10
int depthPerIteration = 30
for (int i = 1; i <= maxIterations; i++) {
LOGGER.warn("Shallow clone detected: deepening by {} commits (iteration {}/{})", depthPerIteration, i, maxIterations)
fetchDeepen(remote, depthPerIteration)
String described = describeHeadWithTags(false)
if (described != null) {
LOGGER.warn("Found version tag after deepening by {} commits", i * depthPerIteration)
return
}
}
LOGGER.warn("Could not find a version tag after deepening by {} commits", maxIterations * depthPerIteration)
}

/**
* Checks if the current branch is tracking a remote branch
* @param remote
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,22 @@ abstract class StatusPorcelain extends GitReadCommand {
/**
* Retrieves a given Git config key with its value for a given scope
*/
/**
* Checks if the current repository is a shallow clone
* ex. git rev-parse --is-shallow-repository -> true/false
*/
abstract class IsShallowRepository extends GitReadCommand {
@Override
String obtain() {
try {
return executeGitCommand("rev-parse", "--is-shallow-repository")
.replaceAll("\n", "").trim()
} catch (Exception e) {
return "false"
}
}
}

abstract class GetGitConfigValue extends GitReadCommand {
private static final Logger logger = LoggerFactory.getLogger(GetGitConfigValue)
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ abstract class CreateTag extends GitWriteCommand {
}


/**
* Fetches additional history from a remote using --deepen
* ex. git fetch --deepen=30 origin
*/
abstract class FetchDeepen extends GitWriteCommand {
@Override
String obtain() {
try {
return executeGitCommand("fetch", "--deepen=${parameters.depth.get()}".toString(), parameters.remote.get())
} catch (Exception e) {
return null
}
}
}

/**
* Creates a tag with a given message
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ interface GitWriteCommandParameters extends ValueSourceParameters {
Property<String> getRemote()
Property<String> getTag()
Property<String> getTagMessage()
Property<String> getDepth()
}